DEV Community

Cover image for NextJS, Payload, and TypeScript in a Single Express Server Boilerplate
James Mikrut for Payload CMS

Posted on • Originally published at payloadcms.com

NextJS, Payload, and TypeScript in a Single Express Server Boilerplate

One of Payload CMS’ core ideologies is that it does not impose any of its own structure on you as a developer. You give it an Express server to use—not the other way around, and this pays dividends in developer freedom and developer experience.

An example of how this approach can be leveraged is by running a full NextJS site on the same Express app as your Payload CMS instance. We’ve built a boilerplate that demonstrates exactly how this works:

Check it out:

https://github.com/payloadcms/nextjs-custom-server

This boilerplate includes the following:

  • Payload CMS and NextJS up and running on a single Express server
  • Super fast Local API usage within pages’ getServerSideProps
  • A demonstration for how to use TypeScript in a Payload and NextJS project
  • Example code for how Payload’s Blocks field type can be leveraged to produce dynamic, layout-builder style pages
  • Page meta data using NextJS’ Head component
  • Payload’s Upload support, including automatic image resizing
  • How Payload’s Local API can be used to seed initial data into your database
  • How Payload’s Rich Text field can be used to map 1:1 to React components
  • TRBL’s ESLint config set up and ready to go
  • Environment variables properly and securely configured using dotenv

When this type of setup is best used

If you know you need a CMS and will be leveraging NextJS in a server-side rendering capacity, and know that you will not be deploying on Vercel, this boilerplate will be perfect for you. This approach can be super valuable and can get you up and running with a full CMS—complete with everything you need to build a modern, blazingly fast site or app including custom validation, full authentication, access control, and much more.

Configuring TypeScript

Much of the complexity that we handle within this boilerplate comes from using TypeScript to build a custom NextJS server. At Payload, we’re big fans of TypeScript (all of Payload is written in TS). We’re doing our best to adopt and embrace it completely, and we think that it’s only going to get more and more popular.

This boilerplate contains two tsconfig.json files:

  • The main tsconfig.json, which will be used for the entirety of your NextJS app, including all your React components
  • The tsconfig.server.json file, which will handle everything in the /server folder

You’ll see that we’ve extended the main tsconfig.json config within the server config and overridden a few properties.

Due to how NextJS relies on dynamic import statements, it requires that its TypeScript projects specify "module": "esnext" in their TS configs. But, Express requires the CommonJS pattern — meaning we have no choice but to require two separate TS configs. No big deal, but this is a common “gotcha” when working with NextJS and TypeScript.

Setting up the server

The Express server itself is pretty simple:


/* eslint-disable global-require */
/* eslint-disable no-console */
import path from 'path';
import next from 'next';
import nextBuild from 'next/dist/build';
import express from 'express';
import payload from 'payload';
import { config as dotenv } from 'dotenv';

dotenv({
  path: path.resolve(__dirname, '../.env'),
});

process.env.PAYLOAD_PUBLIC_SERVER_URL = process.env.SERVER_URL;
process.env.NEXT_PUBLIC_SERVER_URL = process.env.SERVER_URL;

const dev = process.env.NODE_ENV !== 'production';
const server = express();

payload.init({
  license: process.env.PAYLOAD_LICENSE,
  secret: process.env.PAYLOAD_SECRET_KEY,
  mongoURL: process.env.MONGO_URL,
  express: server,
});

if (!process.env.NEXT_BUILD) {
  const nextApp = next({ dev });

  const nextHandler = nextApp.getRequestHandler();

  server.get('*', (req, res) => nextHandler(req, res));

  nextApp.prepare().then(() => {
    console.log('NextJS started');

    server.listen(process.env.PORT, async () => {
      console.log(`Server listening on ${process.env.PORT}...`);
    });
  });
} else {
  server.listen(process.env.PORT, async () => {
    console.log('NextJS is now building...');
    await nextBuild(path.join(__dirname, '../'));
    process.exit();
  });
}
Enter fullscreen mode Exit fullscreen mode

First, we load dotenv and then we expose our SERVER_URL to both NextJS and Payload. Prefixing environment variables with NEXT_PUBLIC_ will ensure that the variable is accessible within NextJS components, and similarly, prefixing a variable with PAYLOAD_PUBLIC_ will expose the variable to Payload’s admin panel.

Important: Only expose environment variables like this if you know that they are 100% safe to be readable by the public. For more information about how Payload allows you to expose environment variables to its Admin panel, click here.

On line 20, we then initialize Payload by passing it a license key (only necessary in production), a long and unguessable secret string used to secure Payload, a URL pointing to our MongoDB instance, and our newly instantiated Express app.

Note: Payload stores your data in MongoDB — so to use Payload, you need to make sure that you have MongoDB up and running either locally or with a third-party platform like MongoDB Atlas.

Serving your app vs. building it

On line 27, we perform different actions based on if the NEXT_BUILD environment variable is set. We do this as a nice-to-have because your Next app is going to be relying on your Payload APIs, especially if it has any static page generation to do. When you go to build your Next app, you probably also need your Payload server to be running.

So, if the NEXT_BUILD variable is set, we start up your Express server for you before allowing Next to build. If it’s unset, we just go ahead and prepare the Next app as usual—then fire up the Express server. Easy peasy.

Layout Building with Blocks

Payload comes with extremely versatile field types that allow you to model any type of data that you need. One of the most capable types is the Block field — and with it, you can allow your content editors to build completely dynamic page layouts with a super streamlined interface right within the Payload admin panel. Admins can then add, remove, and reorder blocks easily based on predefined components that you provide them with.

The beauty of using a JavaScript library like React together with your Payload API means that you can write React components that map 1:1 with your blocks’ data. Your React components can accept the data that your editors author as props, and boom—your layouts are extremely well-organized and extensible well into the future.

In this boilerplate, we’ve pictured how you can even write your Payload block configs directly in the same file as their React component counterparts. You could even go so far as to re-use your frontend website’s React component that shows the data saved within Payload’s admin panel itself to edit that same data. There is a ton of potential here.

For example, check out the Call to Action block in this repo.

In that one file, we define the following:

  • Reusable TypeScript types that correspond with the data within the Block
  • A reusable function to be used with Payload’s Field Conditional Logic to dynamically show and hide fields based on what type of button is selected (custom or page)
  • The Block config itself, describing the fields that are contained within the block. This will be passed to Payload and is the core “definition” of the block
  • The React component to be used on the frontend NextJS site to render the CallToAction block itself

These things don’t all need to be in the same file, but if you want to, Payload allows for it. Both NextJS and Payload support transpiling JSX within their files. You should be able to write your projects however you want.

Here’s how that CallToAction block looks in the Admin panel:

Payload Admin Panel

And here’s how it looks in the minimally styled NextJS frontend:

Layout Building Blocks in React and NextJS

Dynamically rendering Blocks in React

Actually going to render the blocks themselves in React is also pretty trivial:

/components/RenderBlocks/index.tsx:

import React from 'react';
import { Layout } from '../../collections/Page';
import { components } from '../../blocks';
import classes from './index.module.css';

type Props = {
  layout: Layout[]
  className?: string
}

const RenderBlocks: React.FC<Props> = ({ layout, className }) => (
  <div className={[
    classes.renderBlocks,
    className,
  ].filter(Boolean).join(' ')}
  >
    {layout.map((block, i) => {
      const Block: React.FC<any> = components[block.blockType];

      if (Block) {
        return (
          <section
            key={i}
            className={classes.block}
          >
            <Block {...block} />
          </section>
        );
      }

      return null;
    })}
  </div>
);

export default RenderBlocks;
Enter fullscreen mode Exit fullscreen mode

The above component accepts a layout prop that is typed to an array of Payload blocks. The component then maps over the provided blocks and selects a block from those provided by the blockType of each block in the array. Props are provided, and the block is rendered! Beautiful. So simple, and so much power.

Seeding data using Payload’s Local API

This boilerplate comes with an optional seed script which can be run via yarn seed or npm run seed.

It automatically creates one Media document (which uploads and formats a JPG) and two sample Page documents that demonstrate a few Blocks in action.

Payload’s Local API is extremely powerful. It’s got a ton of use cases—including retrieving documents directly on the server within custom routes or within NextJS’ getServerSideProps as seen in the Page component within this boilerplate. It’s super fast, because there’s no HTTP layer: it’s not a typical REST API call or a GraphQL query. It never leaves your server and returns results in a handful of milliseconds, and it’s even faster if you’re running a local MongoDB instance. You thought NextJS server-rendering was fast? Try it when you don’t even need to leave your server to get your data. That’s fast.

You can also use the Local API completely separately from your running server within separate Node scripts.

By passing local: true to Payload’s init() call, Payload will skip setting up the REST and GraphQL APIs and only expose its Local API operations. Perfect for seed scripts and similar programmatic activities like batch-sending emails to customers, migrating your data from one shape to another, manually syncing Customer records to a CRM, etc.

Here’s the seed script that comes with this boilerplate:

const payload = require('payload');
const path = require('path');

const home = require('./home.json');
const sample = require('./sample.json');

require('dotenv').config();

const { PAYLOAD_SECRET_KEY, MONGO_URL } = process.env;

payload.init({
  secret: PAYLOAD_SECRET_KEY,
  mongoURL: MONGO_URL,
  local: true,
});

const seedInitialData = async () => {
  const createdMedia = await payload.create({
    collection: 'media',
    data: {
      alt: 'Payload',
    },
    filePath: path.resolve(__dirname, './payload.jpg'),
  });

  const createdSamplePage = await payload.create({
    collection: 'pages',
    data: sample,
  });

  const homeString = JSON.stringify(home)
    .replaceAll('{{IMAGE_ID}}', createdMedia.id)
    .replaceAll('{{SAMPLE_PAGE_ID}}', createdSamplePage.id);

  await payload.create({
    collection: 'pages',
    data: JSON.parse(homeString),
  });

  console.log('Seed completed!');
  process.exit(0);
};

seedInitialData();
Enter fullscreen mode Exit fullscreen mode

Pretty awesome stuff.

When this boilerplate should not be used

If you plan to next export a fully static version of your NextJS site, then the value of this boilerplate diminishes a bit and you should likely keep your front + backend entirely separate from each other. In this case, the only strength that this approach offers is that you can host your CMS and your app itself on one server, with one deployment. If possible, in this case, you might want to consider deploying your statically exported site on a CDN-friendly host like Netlify, Vercel, or even an S3 bucket, and host your Payload instance on DigitalOcean, Heroku, or similar.

More examples are on their way

We plan to release many more boilerplates in the future, so if this one doesn’t make sense for your needs, make sure to follow along with us to keep up with everything we’ve got coming out, including examples for how to use Payload alongside of a fully static site exported with Next, built with Gatsby, or other similar tactics.

What’s next?

With this boilerplate, you can build fully featured NextJS sites and apps fully powered by a CMS. So get building! Define your own Collections that describe the shape of your data, make use of Payload’s Globals for items such as header and footer nav structures, or build a full user-authenticated app by relying on Payload’s extensible Authentication support.

If you’d rather start a blank Payload project, you can get started in one line:

npx create-payload-app
Enter fullscreen mode Exit fullscreen mode

From there, you’ll be prompted to choose between a few different starter templates in JS or TS.

It’s also super easy to build out a Payload project yourself from scratch.

Let us know what you think

We want Payload to be the best CMS out there for modern JavaScript developers. Since our launch, we’ve received amazing feedback on our ideas and have had an awesome reception from the community, but we’re only getting started. We’d love to hear what you think. Leave a comment here with your thoughts, submit any issues or feature requests you might come across on our GitHub repo, or send us an email. We happily give out pro-bono licenses to open-source projects and nonprofits, as well as personal projects on a case-by-case basis. If this is you, let us know!

Thank you for reading and keep an eye out for more!

Top comments (0)