Does Express Work With Payload CMS?

Partially CompatibleLast verified: 2026-02-26

Express can serve as a custom backend for Payload CMS, but Payload is itself a full framework that makes Express redundant for most use cases.

Quick Facts

Compatibility
partial
Setup Difficulty
Moderate
Official Integration
No — community maintained
Confidence
high
Minimum Versions
Express: 4.17.0
Payload CMS: 2.0.0

How Express Works With Payload CMS

Payload CMS is built on Express internally, so you're not adding Express on top of Payload—you're either using Payload's built-in Express server or extending it. If you need Express explicitly, the typical pattern is to run Payload's REST API headlessly and build a separate Express app as a custom backend layer. This works well when you want middleware control, custom routing, or authentication flows that Payload doesn't provide out-of-the-box. However, since Payload already exposes a full Express instance via its `rest` and `graphql` config, most developers extend Payload's config rather than running parallel Express servers. The developer experience is smoother if you think of Payload as your primary framework and Express as a configuration detail you're customizing, not as a separate tool you're bolting on.

Best Use Cases

Building a headless CMS backend where Payload handles content and a separate Express server handles business logic and third-party integrations
Creating custom middleware for authentication, rate limiting, or request transformation that Payload's hooks don't cover
Serving static assets or proxying requests through Express while using Payload for content management via REST API
Migrating from an existing Express application by replacing its database layer with Payload CMS

Payload with Custom Express Middleware

bash
npm install payload express dotenv
typescript
import express from 'express';
import payload from 'payload';

const app = express();

// Initialize Payload with custom Express config
await payload.init({
  secret: process.env.PAYLOAD_SECRET,
  express: app,
  onInit: async () => {
    console.log('Payload initialized');
  },
});

// Add custom Express middleware/routes
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

app.get('/api/custom', (req, res) => {
  res.json({ message: 'Custom Express route' });
});

// Payload's REST routes are already mounted
// GET /api/collections/:slug, POST /api/collections/:slug, etc.

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Known Issues & Gotchas

critical

Running Express and Payload on the same port causes conflicts; they need separate ports or one must wrap the other

Fix: Either configure Payload to run on a different port, or use Payload's custom Express instance feature via the `express` config option to inject your middleware into Payload's server

warning

Payload includes Express as a dependency, so you're already paying for it—adding explicit Express code may feel redundant

Fix: Leverage Payload's `rest` and `graphql` APIs and use Payload's `express` config to add custom routes directly into Payload's Express instance

warning

Authentication and authorization must be coordinated between Payload and Express if they're separate; token validation logic can diverge

Fix: Use Payload's REST API with Bearer tokens and validate those same tokens in Express middleware using a shared validation function

info

TypeScript type safety is lost when calling Payload REST API from Express without code generation

Fix: Use Payload's generated REST API types or use a tool like OpenAPI Generator to type your API calls

Alternatives

  • Next.js with Payload CMS plugin—native integration, better DX, built-in SSR/SSG
  • Strapi with Express—more Express-first approach, lighter than Payload, easier to extend with raw Express code
  • NestJS with a headless CMS (Contentful, Sanity)—full-featured framework with excellent TypeScript support and no CMS coupling

Resources

Related Compatibility Guides

Explore more compatibility guides