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