Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Remix, Payload in a Single Express Server Monorepo

Published On
Remix Payload monorepo
Remix Payload monorepo
Payload CMS’ Local API is an incredibly powerful tool when building server-side rendered applications.

Payload and Remix in a monorepo setup

Why a monorepo?

Why Remix?

1
// apps/server/index.ts
2
import payload from 'payload';
3
import express from 'express';
4
import { createRequestHandler } from '@remix-run/express';
5
6
// Initialize the Payload instance
7
payload.init({
8
express: app,
9
mongoURL: MONGODB_URL,
10
secret: PAYLOADCMS_SECRET,
11
onInit: () => {
12
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
13
},
14
});
15
16
// Pass any request that wasn't handled by Payload to Remix by utilizing it's createRequestHandler
17
app.all('*', (req, res, next) =>
18
createRequestHandler({
19
...,
20
getLoadContext(req, res) {
21
return {
22
payload: req.payload,
23
user: req?.user,
24
res,
25
};
26
},
27
})(req, res, next)
28
);
1
export const loader: LoaderFunction = async ({
2
context: { payload, user },
3
}): Promise<LoaderData> => {
4
if (!user) {
5
return { users: [], authenticatedUser: undefined };
6
}
7
const { docs: users } = await payload.find({
8
collection: 'users',
9
user,
10
overrideAccess: false
11
});
12
13
return { users, authenticatedUser: user };
14
};

Local API

1
const response = await payload.create({
2
collection: 'statistics',
3
data: {
4
// ...
5
},
6
overrideAccess: true
7
});
1
import { Response } from '@remix-run/node';
2
import type { Page } from '@org/cms';
3
import type { TypedResponse, LoaderFunction } from '@remix-run/node';
4
5
export type LoaderData = {
6
pages: Page[];
7
};
8
export const loader: LoaderFunction = async ({
9
context: { payload, user },
10
request,
11
params,
12
}): Promise<LoaderData | TypedResponse<never>> => {
13
const { pathname } = new URL(request.url);
14
if (pathname === '/') {
15
// Redirect to the home page if no route path is used. This could as easily have been a Payload Global.
16
return redirect('/home');
17
}
18
19
const { pageSlug } = params;
20
const { docs: [page] } = await payload.find({
21
collection: 'pages',
22
user,
23
overrideAccess: false,
24
where: { slug: { equals: pageSlug }},
25
});
26
27
if (!page) {
28
return new Response('Not found', { status: 404 });
29
}
30
31
return { page };
32
};
  • Create an index route which allow the user to navigate to the other pages, or;
  • Simply re-export the default loader and component from our dynamic $page route

The second option looks like:

1
// project/app/routes/index.tsx
2
export { default } from './$page';
1
import type { RootLoaderData } from '~/root';
2
3
export default function Page() {
4
const { page: pageSlug } = useParams();
5
const [{ data }] = useMatches();
6
const { pages, user } = data as RootLoaderData;
7
const page = pages?.find((page) => page.slug === pageSlug);
8
// ...
9
}

Layout Route

SEO

1
import type { MetaFunction } from '@remix-run/node';
2
3
export const meta: MetaFunction = ({ parentsData, params }) => {
4
const { page: pageSlug } = params;
5
const {
6
root: { pages },
7
} = parentsData;
8
9
const page = pages?.find((page) => page.slug === pageSlug);
10
return {
11
title: page?.meta.title,
12
description: page?.meta.description,
13
keywords: page?.meta.keywords,
14
};
15
};

Access Control

Bonuses

Lastly, here are a few tips and tricks that I have found useful when working with Payload and Remix—

Typed Remix context:

1
// In a *.d.ts file that is loaded in your tsconfig.json
2
3
// User is coming from Payload's genereteTypes feature, based on the Users collection
4
declare module '@remix-run/node' {
5
interface AppLoadContext {
6
payload: Payload;
7
user: {
8
user?: User;
9
token?: string;
10
exp?: number;
11
};
12
res: Response;
13
}
14
}
1
// In a Remix login action
2
export const action: ActionFunction = async ({
3
context: { payload, user, res },
4
request,
5
}) => {
6
// ... other code
7
8
await payload.login({
9
collection: 'users',
10
data: { email, password },
11
res, // <-- Pass the response object to Payload like this
12
});
13
14
return redirect('/');
15
};