PayloadCMS and Next.js With Local API
In this video I'm going to show you how to setup Next.js and Payload to run on the same server, kind of like a monolith app, and then how to use Local API to get and filter the data.
When dealing with headless CMSs the usual workflow would be to connect to REST API or use GraphQL to get the data from the API and display it on your frontend.
If you are using Next.js (or some other SSR framework) with PayloadCMS you can take advantage of a feature called Local API.
Local API allows you to get the data directly on the server and then display it on the frontend, which means that you don’t need to deal with server latency or network speed and can interact directly with your database.
In this video I’m going to show you how to setup Next.js and Payload to run on the same server, kind of like a monolith app, and then how to use Local API to get and filter the data.
Next.js and Payload Setup
For this demo we are going to be using official Payload/Next template from Github . Of course you could set this up yourself but that is not a trivial task, so for the sake of time we are going to be using this.
Go to Github, click on “Code” and copy the path to this Github repo.
In your terminal do git clone
and then paste the path to the repo, at the end add the name of your project. I’m going to call mine “payload-next”:
git clone https://github.com/payloadcms/nextjs-custom-server payload-next
Now CD into it, and make sure that you are running at least 16 point something version of Node.
Create .env
file by copying .env.example
file like this:
cp .env.example .env
Let’s open this project in our code editor, and then open the newly created .env
file.
First just add a random string for PAYLOAD_SECERT
variable.
Next, add your MongoDB connection to the MONGODB_URI
variable. If you don’t have MongoDB running on your local environment check the previous video for some ideas. I’m using Docker image of MongoDB for this demo.
Now let’s do npm install
to install all the dependencies.
If you get an error like I did, run npm install
with legacy-peer-deps
flag.
npm install --legacy-peer-deps
Now we just run npm run dev
and we should be good to go.
If you go to localhost:3000
in your browser, you should be greeted with the 404 page. Not to worry, we will fix that later. For now let’s go to the localhost:3000/admin
, this is where Payload Admin UI lives. Create an admin account, and you should get to the Admin Dashboard.
Creating collections
We are getting 404 on the front page of our app because we didn’t setup a home page. Luckily we can seed the database with that data to get rid of that error. Or you can create a homepage manually.
I’m going to go with the seed option in this demo, because I also want to seed the Products, Colors and Sizes that we are going to be filtering with Local API. Before we seed the database we need to create necessary collections.
First I’m going to create Colors
collection.
Create a file called Colors.ts
inside the collections
folder and add this code:
import { CollectionConfig } from 'payload/types';
const Colors: CollectionConfig = {
slug: 'colors',
admin: {
useAsTitle: 'name',
},
access: {
read: () => true,
},
fields: [
{
name: 'name',
type: 'text',
label: 'Color name',
},
],
timestamps: false,
};
export default Colors;
Check the previous video if you don’t understand what is going on here - in short we are just creating Colors collection with one field called name
.
Next create a file called Sizes.ts
also in the collections
folder. And add this code to it:
import { CollectionConfig } from 'payload/types';
const Sizes: CollectionConfig = {
slug: 'sizes',
admin: {
useAsTitle: 'name',
},
access: {
read: () => true,
},
fields: [
{
name: 'name',
type: 'text',
label: 'Size name',
},
],
timestamps: false,
};
export default Sizes;
It’s pretty much the same as Colors.ts
.
Now we are going to create a “Products” collection that is going to be using “Sizes” and “Colors” as relational fields along with Product specific fields.
In the collections
folder create a file called Products.ts
and add this code to it.
import { CollectionConfig } from 'payload/types';
const Products: CollectionConfig = {
slug: 'products',
admin: {
useAsTitle: 'name',
},
access: {
read: () => true,
},
fields: [
{
name: 'name',
type: 'text',
label: 'Products name',
},
{
name: 'price',
type: 'number',
label: 'price',
},
{
name: 'colors',
type: 'relationship',
label: 'Colors',
relationTo: 'colors',
hasMany: true,
},
{
name: 'sizes',
type: 'relationship',
label: 'Sizes',
relationTo: 'sizes',
hasMany: true,
},
],
timestamps: false,
};
export default Products;
This is a very simple collection, which consist of name
and price
fields, with two relational fields that are going to reference “Colors” and “Sizes”.
Great, now all we need to do is register these collections in payload.config.ts
file.
import { buildConfig } from 'payload/config';
import dotenv from 'dotenv';
import Page from './collections/Page';
import Media from './collections/Media';
import Products from './collections/Products';
import Sizes from './collections/Sizes';
import Colors from './collections/Colors';
dotenv.config();
export default buildConfig({
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
collections: [
Page,
Media,
Products,
Sizes,
Colors,
],
});
Seeding the database
Now we are ready to seed the database. If you wanna follow along with this video, you can copy index.js
file from seed
folder from the code used in this video (the link will be in the description below). By doing this you will seed the Database with 30 Products, a few Colors and Sizes along with the Homepage of our site.
To seed the database first stop the server and run it again. So that we have all collections implemented.
Stop the server again, and run:
npm run seed
If you go to localhost:3000
we don’t have 404 error anymore, but, more importantly, in the Admin UI you should now see Colors, Sizes and Products that we seeded.
Cool, we can now start using this data in Next.js with Local API.
Generating types
Since Payload fully supports TypeScript, it would be nice to add typings for “Products”, “Colors” and “Sizes” so that we can use it in our Next.js app. With Payload this is super easy, but we just need to setup the command first.
Add this code to your package.json
file under the scripts
object.
"generate:types": "PAYLOAD_CONFIG_PATH=./payload.config.ts payload generate:types"
Now in your terminal run:
npm run generate:types
And our types will be automatically generated.
You can check payload-types.ts
file to see if the generator added our types. And sure enough they are here, “Products”, “Colors” and “Sizes”.
Nice.
Getting the products
Create products.tsx
file inside pages
folder.
First I’m going to import everything I need. Two things are interesting here, and those are payload
from payload, which we will use to initiate Local API and also Product
type that we just generated.
import React from 'react';
import { GetServerSideProps } from 'next';
import payload from 'payload';
import Head from '../components/Head';
import { Product } from '../payload-types';
interface ProductProps {
products: [Product];
}
Next I’m going to create our component. Using ProductProps
and prepare our products for display. Nothing to fancy here, we are just mapping through the products
that we are going to get from Payload and displaying name
, price
, colors
and sizes
.
const Products: React.FC<ProductProps> = ({ products }) => (
<main className="products-page">
<Head title="Product list" />
<div className="container">
<h2>
Products (
{products.length}
)
</h2>
<ul className="product-list">
{products.map((product) => (
<li
key={product.id}
className="product-item"
>
<h3>{product.name}</h3>
<strong className="price">
$
{' '}
{product.price}
</strong>
<strong>Colors:</strong>
<ul className="sub-list">
{product.colors.map((color) => (<li key={color.id}>{color.name}</li>))}
</ul>
<strong>Sizes:</strong>
<ul className="sub-list">
{product.sizes.map((size) => <li key={size.id}>{size.name}</li>)}
</ul>
</li>
))}
</ul>
</div>
</main>
);
Now it’s time to get our data, remember Local API is only usable on the server, so we need to make our request for the data in getServersideProps
.
export const getServerSideProps: GetServerSideProps = async () => {
};
And in here we can query our products and send them to our component. Like this:
const pageQuery = await payload.find({
collection: 'products',
limit: 30,
});
So here I’m using payload
function that I imported, to find all the products
and limit them to 30 results. limit
property comes in very handy when you are developing pagination for your lists.
Only thing we need to do now is send the Products to our component like this.
return {
props: {
products: pageQuery.docs,
},
};
Before we check this in the browser I will just add a bit of css to style.css
to make this look a bit prettier.
.products-page {
background: #000000;
padding-top: 1px;
padding-bottom: 60px;
color: #ffffff
}
.container {
max-width: 960px;
margin-left: auto;
margin-right: auto;
}
.product-list, .sub-list {
margin: 0;
padding: 0;
list-style: none;
}
.product-list h3 {
font-size: 24px;
margin: 0 0 40px;
}
.product-list strong {
display: block;
}
.product-list strong.price {
font-size: 18px;
margin-bottom: 20px;
}
.product-item {
border: 1px solid #2e2e2e;
border-radius: 10px;
background: #0a0a0a;
margin-bottom: 20px;
padding: 20px;
}
.sub-list {
display: flex;
margin-bottom: 20px;
}
.sub-list li {
margin-right: 20px;
}
Ok. Now let’s check it out in the browser. Great, we are getting our products, with only a few lines of code. This is nice, but Local API is so much more powerful than this.
Filtering products
Getting just a list of products wouldn’t be very useful if we couldn’t filter it in some way.
To filter the products we can use where
property. Say for example that we want to get just the products that cost less that 100 dollars.
We can do that by doing something like this.
where: {
price: {
less_than: 100,
},
},
So we use a where
property, and then say that the price should be less than a hundred. And sure enough now we get only those products that have a price less than hundred.
To access nested properties, like color
or size
we would write something like this:
where: {
'colors.name': {
equals: 'Green',
},
},
Now we should get a list of Products that have a Green color in the colors options array.
Cool.
But what if we want to get the products that have the price less than 100 dollars AND have a Green color option. Easy, we can write our query within an and
array, like this.
where: {
and: [
{
'colors.name': {
equals: 'Green',
},
},
{
price: {
less_than: 100,
},
},
],
},
Now we are getting products that have a price bellow 100 dollars and color option of Green. You can also use an or
array, and not only that, you can nest and
in it so you can do some pretty complicated queries.
Kind of like this.
where: {
or: [
{
'colors.name': {
equals: 'Red',
},
},
{
and: [{
price: {
greater_than: 100,
},
'sizes.name': {
equals: 'XS',
},
}],
},
],
},
Now we are displaying products that either have Red color option OR they have XS size option AND are less than 100 dollars.
Very powerful stuff.
You can take a look at all the options you have available in the documentation. And also keep in mind that these filters are not available only to the Local API, you can use similar syntax when doing GraphQL queries and even REST queries, but for them it is best to use qs
package to stringify your queries so that they don’t look shitty.
Of course Local API is not only used for getting and filtering your data. You can also very easily use it to create, update and delete items. Local API is also used for authentication, so logging in users, resetting passwords and so on.
I can’t cover everything in this video, so checkout the docs if you are interested in other aspects of Local API.