Remix Jokes with a decoupled Node.js API
This project demonstrates a full-stack web application architecture where the Remix front end is fully decoupled from a standalone Node.js/Express back-end API. Rather than relying on Remix’s built-in server-side data loading to talk directly to a database, the application introduces a separate Express API layer that handles all business logic, data access, and authentication independently. A detailed walkthrough of the architecture and reasoning is available in the accompanying blog post.
Why Decouple the Backend?
The standard Remix approach encourages loading data directly in route loaders, which works well for many applications. However, in larger or more complex systems, there are strong reasons to separate concerns. A decoupled backend can be shared across multiple clients (web, mobile, third-party integrations), can be scaled independently from the front end, and allows backend and frontend teams to work in parallel with clearly defined API contracts. This project explores that pattern using Remix as the frontend framework and Express as the API server.
Technical Architecture
The backend API is built with Express and Node.js, using Prisma as the ORM for type-safe database access against a PostgreSQL database. Authentication is handled via JWT access tokens paired with refresh tokens stored in Redis, providing a secure and stateless authentication flow. The Remix frontend communicates with the Express API over HTTP, treating it as any external service would.
For deployment, the stack targets AWS infrastructure: AWS Elastic Beanstalk for hosting the application and AWS RDS for the managed PostgreSQL database. GitHub Actions handle continuous integration and deployment workflows.
Monorepo and Multirepo Approaches
The project was implemented in two distinct repository structures to explore the trade-offs of each approach.
Monorepo project
The monorepo approach keeps both the Remix frontend and the Express backend in a single repository. This structure allows both packages to share Prisma-generated TypeScript types directly, since the Prisma Client dependency lives in the root node_modules folder. Shared types eliminate the risk of type drift between frontend and backend, making refactoring safer and faster.
Multirepo project
The multirepo approach separates the frontend and backend into independent repositories. While this provides cleaner separation of concerns and independent deployment pipelines, it introduces the challenge of sharing Prisma-generated types across repository boundaries. The initial solution uses a manually maintained type definition file, though more robust approaches such as publishing a shared npm package, using git submodules, or creating symlinks are all viable alternatives.
Key Challenges
One of the primary challenges was managing type safety across the decoupled boundary. When the frontend and backend share a database schema but live in separate codebases, keeping TypeScript types synchronised requires deliberate tooling decisions. The monorepo structure solves this elegantly, while the multirepo structure highlights the real-world complexity of maintaining type contracts across service boundaries.