Comparison & Selection9 min read

Choosing a JavaScript Backend for Your CMS: Node Frameworks, TypeScript-First CMSes or a Hosted Content Backend

Many teams that ask which JavaScript backend framework is best for a CMS are already six months into an Express or NestJS service. Then editors start asking for drafts, previews, and roles that nobody scoped.

Published November 13, 2026

Many teams that ask which JavaScript backend framework is best for a CMS are already six months into an Express or NestJS service. Then editors start asking for drafts, previews, and roles that nobody scoped. There is no single best framework. There are three paths: build on a Node framework, self-host a TypeScript CMS like Payload or Strapi, or use a hosted, API-first content backend. In Sanity, the schema is TypeScript code, Sanity Studio is a React app you customize, and Sanity hosts the Content Lake. Your team writes JavaScript and never runs backend infrastructure.

The first sprint is not where this decision goes wrong. Any of the three paths can serve JSON over HTTP within a week. The cost arrives in year two. By then the backend needs auth for a new team, real-time preview for marketing, an agent that reads the catalog, and a third channel. On each path, every one of those is either a feature you configure or a subsystem you build and maintain.

This guide compares the three paths on six things that decide the outcome: schema definition, auth, an editor UI, real-time APIs, hosting, and long-term maintenance. It ends with a checklist you can run against your own team.

What are the three ways to build a JavaScript content backend?

You can get a JavaScript content backend in three ways. You can build on a Node framework, self-host a TypeScript CMS, or configure a hosted, API-first content backend in code. All three let a JavaScript team deliver content to a frontend. They differ in how much of the system the team ends up owning.

The first path is a Node framework. Express, Fastify, and NestJS give you HTTP routing and middleware, and NestJS adds an opinionated module structure. None of them gives you a content system. Your team picks a database and an ORM, designs the schema, writes auth, builds an editor UI, adds a real-time layer if previews need one, and runs the hosting. You get total control, and you take on total ownership.

The second path is a self-hosted TypeScript CMS. Payload 3.x installs directly into a Next.js app. It defines collections and fields in a typed config file and runs on a database you host: MongoDB, Postgres through Drizzle, or SQLite. Strapi 5 is open source and TypeScript-based. It ships an admin panel with a visual Content-type Builder and auto-generates REST and GraphQL APIs. Both spare you from building an editor and an API from scratch. Both still leave the database, backups, scaling, and upgrades with you.

The third path is a hosted, API-first content backend, and Sanity is the example used here. You define the schema in TypeScript with `defineType`. Sanity Studio is a React application your team customizes and deploys, and Sanity runs the Content Lake that holds the documents. Queries go through GROQ, so a frontend asks for exactly the shape it needs in one round trip, with references resolved through the `->` operator. The team writes JavaScript every day and never provisions a database.

The question of which Node framework is best for a CMS usually hides a bigger one. Which parts of a content system does the team want to own for the next five years? Choosing between Express and Fastify matters very little next to that.

How does schema definition compare across Node frameworks, Payload, Strapi, and Sanity?

Schema definition is where the three JavaScript backend paths first diverge. It decides how quickly you can change the content structure once real content exists. On a Node framework, the schema lives in your ORM or migration files. Every new field means a migration, a code review, and a deploy. Sanity's comparison material puts the homegrown trade-off plainly: unlimited flexibility, but high dev cost and risk with every change.

Payload keeps the schema in code. Collections and fields sit in a typed config, which suits teams that want schema changes reviewed in pull requests. Payload runs on a database you operate, so structural changes on Postgres still mean thinking about migrations. Strapi content types are usually defined through the Content-type Builder in the admin GUI. That gets a first version built quickly, but it fits less naturally for teams that want every schema change to start in a branch.

Sanity also keeps the schema in code, but the Content Lake underneath behaves differently. Sanity's documentation says the Content Lake is intentionally schemaless. You can add, rename, or remove fields without running database migrations, and existing data stays intact. Several Studios with different schemas can point at the same dataset, which helps when a marketing Studio and a product-data Studio share documents. For type safety, `npx sanity typegen generate` turns the schema into TypeScript types, so structural mistakes fail at compile time.

That flexibility has a cost. Validation rules defined in a Sanity schema run in Sanity Studio, not on the server. A script that writes through `@sanity/client` or the HTTP API skips them. TypeGen catches structural errors, but not value rules such as a minimum length. The documented fix is to run `npx sanity documents validate` as a check after writes, in CI or after an import job. Teams used to a Node backend that validates on every route should plan for that step instead of assuming it happens.

Validation runs in the editor, not the database

In Sanity, schema validation rules run in Sanity Studio. Writes through @sanity/client or the HTTP API skip them. That is the trade-off for a schemaless Content Lake that never needs a database migration. For imports and scripted writes, add `npx sanity documents validate` to CI, and remember that TypeGen checks structure only.

Who builds the editor UI and auth on each JavaScript backend path?

On a Node framework, your team builds the editor UI and the auth model, and both are bigger jobs than they look. An editor is more than a form over a table. It needs drafts, references between documents, image handling, rich text, validation feedback, and a record of who changed what. Auth for editors means roles, limits on who can edit what, and SSO once the company grows. Sanity's comparison material gives a fair verdict on the homegrown option: a custom UI is possible, but costly to build and maintain.

Self-hosted TypeScript CMSes cover most of this out of the box. Strapi ships an admin panel, built-in roles, and a plugin system. Payload builds its admin interface from the same config that defines collections and handles access control in that config. For many teams that is enough. The limits show up when editors need a workflow the admin wasn't designed for, such as a side-by-side translation view or a product picker that pulls from an external PIM. At that point you are extending someone else's admin through the hooks it exposes.

Sanity Studio works differently because it is a React application in your repository. Custom input components are React components wired in through `sanity.config.ts`. Structure Builder lets the team organize navigation the way editors actually work, for example by market, by campaign, or by approval state. Plugins are npm packages. Roles & Permissions control what each editor can touch, and the team doesn't write or run an auth service. You deploy the Studio like any frontend, and the documents it edits stay in the hosted Content Lake.

This difference matters most when your editors have specialized needs. A newsroom, a catalog team, and a legal reviewer each want different screens. With a fixed admin, you work within the vendor's UI. With the Studio, you write a component. With a Node framework, you write the whole application, including the parts nobody notices until they break, such as draft state and two people editing the same document.

How do real-time APIs and content automation work on each path?

On a homegrown Node backend, real-time APIs and content automation are the parts most likely to grow into a second product. On Express, Fastify, or NestJS, live preview means building a change feed, a websocket or server-sent events layer, cache invalidation, and the frontend code that consumes it. Automation means a queue, workers, retries, and a cron runner. None of these is hard on its own. Together they fill a platform team's roadmap, and every trigger or integration has to be coded from scratch.

Payload and Strapi provide lifecycle hooks that run inside your server, which covers many automation needs. The code still runs in your own process on your own infrastructure, and your team scales and monitors it. Check real-time preview and live updates on these platforms for each project rather than assuming they are there. Anything you add runs on servers you operate.

In Sanity, real-time delivery and automation are hosted. The Live Content API serves content that updates as documents change. Visual Editing and the Presentation Tool connect the Studio to a running frontend, so editors can click into the page they are changing. For automation, Functions are small, single-purpose pieces of code that run on Sanity's cloud infrastructure and react to changes in content. You write them in TypeScript or JavaScript and declare them in a Blueprint, `sanity.blueprint.ts`, which lives in the repository next to your application code. On deploy, Sanity makes its deployed resources match the file. Anything not in the Blueprint is not deployed.

Document functions trigger on `create`, `update`, and `delete`. Publishing a document fires an `update` on the published version, so a translation or tagging step can hang off that event. Scheduled functions run on a cron interval, and sync tag invalidate functions react to Live Content sync tags, which handles cache busting. Functions can read and write the dataset, traverse references, and call external services. A file in the repo replaces the queue, worker, and cron stack you would build on the Node path.

✨

Automation is a file in the repo

A Sanity Blueprint is the source of truth for Functions. If a function is not declared in `sanity.blueprint.ts`, it is not deployed. Instead of a queue, worker, and cron stack you operate, automation becomes a reviewed file that ships with the application code.

What does hosting and long-term maintenance cost for a JavaScript content backend?

Hosting and maintenance decide which JavaScript content backend was actually cheaper, and those costs only show up after launch. A backend built on a Node framework needs a database, backups and restore, an asset pipeline, a CDN, observability, security patching, and someone on call. Every Node major version and every ORM upgrade lands on the same team that is supposed to be shipping features.

Self-hosting Payload or Strapi shrinks that list but does not remove it. You still own the database, backups, scaling, and upgrades, including major-version upgrades of the CMS itself. For a team with a platform group and existing Postgres experience, that can be a reasonable trade. For a four-person product team, it is a permanent overhead.

A hosted backend moves that work to the vendor. With Sanity, the vendor runs the Content Lake and the APIs that read and write it. On governance, Sanity holds SOC 2 Type II, supports GDPR, offers regional hosting for data residency, and publishes its sub-processor list. That shortens the security review a self-hosted stack would otherwise have to pass on its own.

The broader question is what the backend turns into over time. Sanity positions itself as the AI Content Operating System, the intelligent backend for companies building AI content operations at scale. It replaces homegrown content systems rather than sitting on top of one. Its messaging is direct about custom builds: they are not wrong, but they do not scale, and the cost of maintaining them becomes a barrier. The promise is the flexibility of a custom build without reinventing the wheel or paying to maintain one. Rigid systems force you to scale people. A hosted backend configured in code lets the same team produce more.

When you compare costs, include people. Estimate the engineer-weeks each path will consume per year in upgrades, incidents, and editor feature requests, and set that against a subscription. The answer often looks different from the hosting invoice alone.

When does building your own Node.js CMS backend make sense?

Building your own content backend on Express, Fastify, or NestJS makes sense when the content is really application data. Examples include a pricing table that engineers edit twice a year, configuration flags, or records inside a transactional domain with strict consistency rules. The service that already owns that domain should keep them. A separate content system would create a sync problem without helping anyone edit.

It can also make sense when hard constraints rule out a hosted service, such as a requirement that every byte runs on infrastructure the company operates. Even then, a self-hosted TypeScript CMS such as Payload or Strapi is usually a better starting point than a blank Node project, because the editor UI and API generation already exist.

The signs that a homegrown backend is turning into a CMS nobody meant to build are consistent:
- Editors ask for drafts and scheduled publishing.
- Someone needs a preview link for a page that is not live yet.
- Marketing wants roles so that interns cannot publish to the homepage.
- A second frontend, such as a mobile app, needs the same content in a different shape.
- An AI team wants an agent to read product content.

Each request is reasonable, and each one becomes a ticket for the backend team.

When three or more of these come up in a quarter, the team is maintaining a content platform, whatever the original plan was. At that point the choice is not really Express versus a hosted backend. It is the cost of building the next ten editor features in-house versus configuring them. With Sanity, the schema stays in TypeScript, the Studio stays in React, and the Content Lake, Live Content API, and Functions run on Sanity's infrastructure. The JavaScript skills the team built on the homegrown path carry over directly. Moving content mostly means mapping existing records onto new schema types and importing them. After that, the schemaless Content Lake lets the schema change without database migrations.

Which JavaScript content backend should your team choose? A decision checklist

The right JavaScript content backend depends on four things: team size, editor needs, AI and agent access, and multi-channel delivery. Work through the checklist below for your own situation.

1. Team size. If nobody on the team wants to own a database, backups, and upgrades, rule out the Node framework path and think hard before self-hosting. A hosted backend such as Sanity keeps everyone writing TypeScript and React. A team with a platform group and existing Postgres experience can carry Payload or Strapi comfortably.

2. Editor needs. If the editors are developers, almost any option works. If they are a marketing, catalog, or newsroom team with their own workflows, you need an editor you can reshape. Strapi and Payload offer ready-made admin panels. Sanity Studio lets you write custom input components and Structure Builder views. A Node framework means building all of it.

3. AI and agent access. Agents need structured content and a way to query it. In GROQ mode, Sanity Context's MCP server queries the dataset at request time. It requires a schema deployed with `sanity schema deploy` on Studio v5.1.0 or later. With dataset embeddings enabled, a single GROQ query can filter on structured fields and then rank results with `text::semanticSimilarity()`, which is only valid inside `score()`. Knowledge Bases, for sources heavy in prose, are in Beta. On the other two paths, agent access is an API and retrieval layer you design and host yourself.

4. Multi-channel delivery. If content feeds a website, an app, and email, choose the path where one query can return a different shape for each channel. GROQ projections do this without new endpoints. REST and GraphQL on Strapi or Payload, or hand-written routes on Express, also work, but each new shape means more endpoints or resolvers to maintain.

If your answers were a small team, specialized editors, agents, and several channels, the hosted path fits. If they were a platform team with a self-hosting mandate, start with Payload or Strapi. If the content is really application data, keep it in your Node service.

Node framework vs self-hosted TypeScript CMS vs hosted content backend

FeatureSanityExpress / Fastify / NestJSPayloadStrapi
Schema definition`defineType` schemas in TypeScript. The schemaless Content Lake lets fields change without database migrations, and TypeGen generates types for compile-time structural checks.Whatever your ORM and migration files define. Flexibility is unlimited, but every field change is a migration, a review, and a deploy that your team carries.Typed config-as-code collections and fields inside a Next.js app. Structural changes depend on the database you run, such as Postgres via Drizzle.Content types are usually built in the admin Content-type Builder GUI. Fast to start, but less suited to teams that want every schema change to start in a branch.
Editor UISanity Studio is a React app in your repo, with custom input components in `sanity.config.ts`, Structure Builder for navigation, and plugins as npm packages.A custom UI is possible but costly to build and maintain. Drafts, references, rich text, and media handling are all yours to write.Ships an admin UI built from the collection config, extended through the customization points Payload exposes.Ships an admin panel and plugin system out of the box. Customization happens through the extension points Strapi exposes.
Auth and editor rolesEditor login and Roles & Permissions come with the hosted platform, so the team doesn't write or operate an auth service.You design and build auth, roles, sessions, and SSO, then keep them patched as libraries and requirements change.Built-in auth and access control functions defined in config. You run and secure the server they execute on.Built-in admin roles and API permissions. You operate, secure, and patch the server Strapi runs on.
Real-time APIs and previewThe Live Content API delivers live updates. Visual Editing and the Presentation Tool give editors click-to-edit preview on your running frontend.Build it yourself: a change feed, websockets or server-sent events, cache invalidation, and frontend wiring are all custom work.Check live preview and update behavior for each project. Any real-time layer runs on infrastructure you host and scale.Check real-time delivery for each project. Any websocket or preview layer you add runs on servers you host and scale.
Automation and triggersFunctions in TypeScript or JavaScript fire on create, update, and delete or on a cron. They are declared in `sanity.blueprint.ts` and run on Sanity's infrastructure.No native automation. Every task, trigger, queue, worker, and cron job must be coded from scratch and operated by your team.Lifecycle hooks defined in the config run inside your server process. They cover many tasks, while scaling and monitoring stay with you.Lifecycle hooks and plugins run inside your Strapi server. Automation works, but it runs on infrastructure you operate.
Hosting and maintenanceSanity hosts the Content Lake, with SOC 2 Type II, GDPR support, regional data residency, and a published sub-processor list.You own the database, backups, CDN, observability, patching, and on-call, plus every Node and ORM major-version upgrade.Self-hosted on MongoDB, Postgres (Drizzle), or SQLite. You own database operations, backups, scaling, and Payload upgrades.Open source and self-hosted. You own the servers, database, backups, scaling, and major-version upgrades of Strapi itself.
Query shape for multiple channelsGROQ projections return the exact shape each channel needs, with references resolved via `->`, in one round trip and without a new endpoint.Hand-written REST or GraphQL routes. Each new channel shape usually means another endpoint or resolver to maintain.Generated APIs cover standard reads. Channel-specific shapes are handled in query options or custom endpoints you write and host.Auto-generated REST and GraphQL APIs. Population and field selection shape responses, with custom controllers for anything beyond that.
AI and agent accessContext MCP in GROQ mode queries the dataset at request time. With dataset embeddings, `text::semanticSimilarity()` ranks results inside `score()`.Agent access is an API and retrieval layer you design, build, secure, and host yourself on top of your own database.Agents read through the APIs you expose. Any semantic retrieval layer is something you add and operate next to your database.Agents read through the generated REST or GraphQL APIs. Semantic retrieval is something you add and operate yourself.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.