Does Express Work With Payload CMS?
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
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
Payload with Custom Express Middleware
npm install payload express dotenvimport 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
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
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
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
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