Color Mode


    Language

Building Backend as a Frontend Developer: Experience with Payload CMS

November 13, 2025

When I was tasked with implementing a headless CMS to primarily serve a mobile application at scale, I found myself stepping into unfamiliar territory. As a frontend developer, backend development once felt like a daunting mix of database design, API implementation, authentication systems, and deployment infrastructure.

Payload offered an approach that leveraged existing TypeScript skills while providing necessary backend capabilities. After deploying to production and maintaining the system through several high-traffic periods, this post reflects on the practical realities of building backend systems with Payload.

What is Payload?

Payload is an open-source, headless content management system (CMS) built with TypeScript and Next.js. Unlike traditional CMS platforms, such as WordPress or Drupal, a headless CMS delivers content via APIs, allowing any frontend stack to consume the content.

A major selling point of Payload is its code-first philosophy. While other headless CMS, such as Contentful and Strapi, require defining schemas through their admin panels, Payload lets you define everything in TypeScript. This approach appeals to developers who are comfortable with modern Javascript frameworks and want to extend the same development experience to their backend infrastructure.

Built on top of Next.js, Payload takes advantage of the framework's server-side rendering and API routing, while also providing its own database abstraction layer. It supports both PostgreSQL and MongoDB, offers REST and GraphQL APIs, and can be deployed on serverless platforms like AWS Lambda or on traditional servers.

For frontend developers stepping into backend work, this means you can manage content, define data structures, and expose APIs all within a familiar TypeScript environment, without needing learn a completely new backend stack.

Configuration-as-Code

Payload differentiates itself through its management of schema definitions as code, as opposed to GUI configurations or SQL statements. In Payload, collections are Typescript configuration objects, with strongly-typed fields that are automatically translated into database table columns.

export const Articles: CollectionConfig = {
  slug: "articles",
  fields: [
    {
      name: "title",
      type: "text",
      required: true,
    },
    {
      name: "body",
      type: "richtext",
    },
    {
      name: "category",
      type: "relationship",
      relationTo: "categories",
    },
  ],
};

From a frontend developer’s perspective, this syntax feels familiar — more like defining component props than database schemas. Additionally, because the code lives in version control, changes can be tracked, reviewed collaboratively, and rolled back if necessary.

The collection above automatically generates several artifacts:

Database Schema

Database tables are created based on the field configurations. The collection above will produce an articles table with the following columns:

REST API Endpoints

Every collection will also receive a complete REST API without additional configurations:

GET    /api/articles          # Retrieve list with pagination
GET    /api/articles/:id      # Retrieve single entity
POST   /api/articles          # Create new
PATCH  /api/articles/:id      # Update existing
DELETE /api/articles/:id      # Delete entity

Default query parameters are also provided for easy filtering and sorting. For instance, in order to retrieve the first 10 articles containing "frontend" in their title, sorted by the creation date:

/api/articles?where[title][contains]=frontend&sort=createdAt&limit=10

TypeScript Type Definitions

Type interfaces are also generated based on the collection configuration, providing type checking and autocomplete support in modern IDEs.

export interface Articles {
  id: number;
  title: string;
  body?: {
    root: {
      type: string;
      children: {
        type: string;
        version: number;
        [k: string]: unknown;
      }[];
      direction: ("ltr" | "rtl") | null;
      format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
      indent: number;
      version: number;
    };
    [k: string]: unknown;
  } | null;
  category?: {
    relationTo: "category";
    value: number | Category;
  } | null;
  createdAt?: string | null;
  updatedAt?: string | null;
}

This automatic artifact generation eliminates a considerable amount of boilerplate code and maintenance, making backend development faster and more reliable.

Reducing the Backend Learning Curve

Several Payload features make backend development more approachable for frontend developers:

Built-in Admin Panel

Payload includes a pre-built, auto-generated admin panel that leverages Next.js Hot Module Replacement for instant feedback during development. The production-ready admin panel automatically reflects TypeScript schema changes and is highly customizable for projects with complex UI requirements.

Payload's default admin panel dashboard
"Create New Article" page

Internationalization and Localization

Supporting multiple languages is handled through configuration rather than complex setup. Fields can be marked as localized, allowing content editors to manage translations directly in the admin panel. The system automatically handles locale-specific queries and provides fallback behavior when translations are missing.

export const Articles: CollectionConfig = {
  slug: "articles",
  fields: [
    {
      name: "title",
      type: "text",
      localized: true, // Enable per-locale content
    },
  ],
};

API requests can specify locale preferences via query parameters, and Payload handles the underlying database queries to retrieve the appropriate content version. For instance, requesting /api/articles?locale=ja&fallback-locale=en returns Japanese content when available, falling back to English otherwise.

Data Relationship Modeling

Creating relationships between data structures uses the relationship field type, as shown in the sample collection above. Payload handles the underlying database joins and provides a functional admin UI for selecting related content.

Hooks

Collection-level and field-level hooks (beforeRead, afterRead, beforeChange, afterChange, etc.) mimic React lifecycle methods, allowing you to implement business logic with relative ease.

export const Articles: CollectionConfig = {
  slug: "articles",
  fields: [
    {
      name: "title",
      type: "text",
      required: true,
    },
    {
      name: "slug",
      type: "text",
    },
    //...
  ],
  hooks: {
    beforeChange: [
      ({ data }) => {
        data.slug = data.title.toLowerCase().replace(/\s+/g, "-");
      },
    ],
  },
};

Custom Endpoints

Beyond the standard CRUD operations, you can define custom endpoints to handle more complex workflows.

export const Articles: CollectionConfig = {
  slug: "articles",
  fields: [
    /* ... */
  ],
  endpoints: [
    {
      path: "/:id/summary", // Define endpoint
      method: "get", // Define method
      handler: async req => {
        if (!req.user) {
          return Response.json({ error: "Unauthorized" }, { status: 401 });
        }
        const article = await req.payload.findByID({
          collection: "articles",
          id: req.routeParams.id as string,
        });

        // Execute complex operation, call external API, etc.
        const summary = await generateAISummary(article.body);

        return Response.json({ success: true, summary });
      },
    },
  ],
};

Access Control

Authorization can be enforced at both collection and field levels, ensuring security is handled consistently at the data layer rather than relying on UI visibility.

export const Articles: CollectionConfig = {
  slug: "articles",
  access: {
    // Allow anyone to read "published" articles
    read: ({ req: { user } }) => {
      if (user) return true;
      return { status: { equals: "published" } };
    },
    // Only allow authenticated users to create, update, or delete articles
    create: ({ req: { user } }) => Boolean(user),
    update: ({ req: { user } }) => Boolean(user),
    delete: ({ req: { user } }) => Boolean(user),
  },
};

This declarative approach makes access control simpler and more maintainable than manually checking permissions in every route handler. Security is enforced consistently for every request, giving you confidence that your data layer is protected.

Caveats and Challenges

While Payload provides a compelling developer experience, moving to production surfaces several challenges worth considering.

Database & Schema Management

During development, the TypeScript-to-database workflow feels seamless, but in production with PostgreSQL, explicit migration management is required. Payload provides migration scripts, but you need to run payload migrate:create for every schema change and carefully manage the order of migrations across environments. This can add coordination overhead when multiple developers are updating schemas simultaneously.

Serverless Deployment Complexity

Deploying Payload as a serverless function can be more complex than expected. Cold starts can impact performance, but in a live production service, they are generally not an issue with typical traffic patterns and proper memory allocation. Additionally, Payload does not natively support scheduled tasks or batch jobs in serverless environments, so features like scheduled publishing require building separate infrastructure using Lambda functions and EventBridge scheduling.

React and Next.js Integration

Since Payload is built on Next.js with the App Router, you also inherit React Server Component challenges. Custom admin panel components must carefully navigate the server/client boundary, and hydration mismatches can occur when building custom fields that interact with browser APIs. If you've struggled with Next.js hydration errors before, expect to encounter them here as well.

Conclusion

As a frontend developer, working with Payload felt like extending my TypeScript toolset rather than learning an entirely new backend framework. Its familiar patterns, automatic API generation, and strong schema typing made backend development more approachable than expected.

There are still challenges — managing migrations, optimizing serverless deployments, and understanding the underlying database structures take deliberate effort. However, Payload narrows the gap between frontend and backend development by letting you apply existing TypeScript experience to backend systems. Payload doesn't cover everything — advanced database optimization, distributed systems, and infrastructure management require dedicated learning. But it offers a practical entry point.

For developers comfortable with React and modern build pipelines, it proves that backend systems can be built without leaving familiar territory.


Payload, the Payload design, and related marks, designs, and logos are trademarks or registered trademarks of Payload CMS, Inc. in the U.S. and other countries.

cmspayload-cmsbackendweb-development

Author

Patrick Dan Lacuna

Patrick Dan Lacuna

Web Frontend Tech Lead

Stuck in a pipeline between frontend and backend development

You may also like

August 26, 2025

Real-time data processing, part 1 - AWS ECS App Mesh and Retry Strategies

In today's fast-paced data-driven world, real-time data processing has become indispensable for businesses across various sectors. From monitoring system performance to analyzing customer behavior, the ability to process data in real-time offers invaluabl...

Serigne Mbacke Ndiaye

Serigne Mbacke Ndiaye

Infrastructure

November 7, 2024

Introducing Shorebird, code push service for Flutter apps

Update Flutter apps without store review What is Shorebird? Shorebird is a service that allows Flutter apps to be updated directly at runtime. Removing the need to build and submit a new app version to Apple Store Connect or Play Console for review for ev...

Christofer Henriksson

Christofer Henriksson

Flutter

ServicesCasesAbout Us
CareersThought LeadershipContact
© 2022 Monstarlab
Information Security PolicyPrivacy PolicyTerms of Service