In this post, we’ll look at an example of a monorepo with multiple apps; a Remix and Payload application communicating through the Local API, served by the same Express server.
Recently, there have been many frontend frameworks with SSR (server-side rendering) capabilities released. One of those is Remix, which uses React for it's rendering. Integrating Payload with such framework has, in my case, turned out fantastic. Therefore I wanted to share my thoughts and an example of such setup.
All the code can be found in this repository as a template for your next project. Consult the project documentation for more information about the technologies used, and it's setup.
Since I learned that Payload has a local API, which I certainly find is one of the best features of Payload, I have thought about the best way to use it.
While it is possible to set up a repository with a single package where all your dependencies is installed, this could cause a few problems. React for example, is a peer dependency of Remix, which means that you are installing React yourself in order for Remix rendering to work. What if you wanted to write custom Payload components, and it turns out that Remix and Payload requires different versions of React? You would need to use package manager's aliasing feature as a work around. This would mostly work, but there is a better solution.
A monorepo lets you define multiple packages, where each package requires their own set of dependencies. The Remix application may require version 17 of react, while the Payload application requires react 18. This flexibility in dependency management make it worth paying for a more complex project setup, in this case a monorepo managed using Turborepo.
The way Payload can be integrated with Remix turns out to be very elegant. In the example repository, since we are running the Remix and Payload instances in the same express server (you guessed it) we can use Payload's Local API.
This is how you integrate Payload with Remix:
By returning the Payload instance from Remix’s getLoadContext
, we can use Payload Local API from any Remix Action or Loader (which is used to load and mutate data on the server side in Remix).
A subtle but significant advantage of this integration is that Remix don't have to know about or bundle a single line of code from the Payload project in order to work. This is because the Payload instance is passed to Remix during runtime.
The user in above example is actually originates from Payload as well, meaning we are using the Payload authentication system in our Remix application. This is accomplished by adding the Payload authentication middleware to Remix's Express routes.
Read more about this in the Payload documentation.
Apart from not having to use HTTP in order to communicate with your CMS and the performance benefits that comes with, this is also noteworthy:
The local API gives us the ability to bypass access control, when needed!
Imagine having a statistics
collection which only administrators have CRUD (create, read, update and delete) access to. How would you register usage statistics for users other than administrators?
When using the HTTP API's there are of course solutions to this, like authorizing the statistics API through the usage of ab API key instead (yes, Payload have you covered here as well) or using collection hooks. But it certainly is a lot easier to simply bypass the Payload access control in the local API:
While this functionality should be used with care, it is really nice to have available in your tool set.
Like NextJS, Remix is using a file based routing system. This means that you can create a file in the app/routes
folder and Remix will automatically create a route for it. The example project have a Pages
collection that we would like to use as pages in our application that the user should be able to navigate between.
In order for our Remix application to use the Pages
collection as routes, we will use the following routing features in Remix:
The Pages collection has a slug
field which is automatically populated based on the title (through a beforeChange hook), which we can use as a dynamic segment in our route.
First of all, let's create a dynamic route in Remix:
project
└── apps
└── web
└── app
└── routes
└── $page.tsx
Now we simply need to fetch the correct page from Payload and render it.
With the loader defined, the page
can now be used inside our route component through Remix`s useLoaderData
hook. If you are interested in an example of this, take a look at the example project. We do have a small issue here though, even though we try to redirect any user visiting the "/
" route, this will currently not happen. This is because Remix is mapping the "/
" route to an index route, which we currently don't have.
To resolve this we can either:
$page
routeThe second option looks like:
Now, our $page
route will be rendered when the user visits the "/
" route as well.
In the example project, we have refactored the loader above from being defined in the $page
route into the root
route instead. This is simply because we want to be able to create a navigation menu with all the pages, not only the current page the user is visiting.
Even though we fetch the pages in the root route, we can still access them in the $page
route through the useMatches
hook:
Remix have a concept of Layout Routes, which is a way to wrap your routes with a layout component. A Layout Route is simply a react component that wraps your subroutes. This is useful for things like a navigation bar, or a footer. Since we already have a $page
route that acts like a global route for all our pages, we want to add a layout route to wrap the $page
route. Layout routes are named the same as the static part of subroutes they wrap. This is a problem for us, since we only have a dynamic route.
In order to solve this, we can use a Pathless Layout Route. The file structure looks like this:
project
└── apps
└── web
└── app
└── routes
└── __page
└── $page.tsx // Dynamic route
└── index.tsx // Re-export the default loader and component from our dynamic `$page` route
└── __page.tsx // Pathless Layout Route
In the example project we render the navigation menu in the Layout Route, and the page content in the $page
route.
So, you have done a good job defining fields in your collections in regards to SEO, such as page title, description and keywords, right?
In Remix we can simply export a meta
function from a route component, and when it renders, it will automatically add the meta tags that we return from the function to the page. Since we have already loaded all the pages in the root route, we can export a meta
function like this, from the $page
route:
We automatically seed two users and two pages into the database on the first startup, one user with the role admin
and one with the role user
. The admin
user can access the everything (including the admin UI), and the user
user can only access the public pages in the Remix application.
Try logging in to the Remix application as the user
user, and you will see that you can only access the home page, while the admin user can access all the pages. This is all handled in the access control functions in Payload.
The developer experience of defining a single source of truth for you access control, which then cascades down to your API, admin UI and Remix application is amazing, in my personal opinion.
We can't wait to see what you build with it!
Lastly, here are a few tips and tricks that I have found useful when working with Payload and Remix—
Typed Remix context:
Let Payload set a User on the response object, when performing the Login operation through the Local API: