Skip to content
← Back to portfolio

Project case study · Web system

Remix Jokes: Decoupled Express API

A personal Remix Jokes tutorial exploration: a decoupled Remix frontend and Express API with JWT auth, Redis-backed refresh-token revocation, Prisma, and PostgreSQL.

RemixExpressNode.jsJWTRefresh TokensRedisPrismaPostgreSQLAWS Elastic BeanstalkAWS RDS

Remix Jokes with a decoupled Node.js API

This is a personal, solo exploration based on the Remix team’s Jokes tutorial, not a production system. I deliberately kept the data model small: User, Password, and Joke. I used it to explore what changes when a tutorial-style Remix app talks to a standalone API over HTTP.

Why decouple the backend?

The Remix frontend calls a separate Express/Node.js API for authentication, users, and joke reads and writes. That makes the service boundary explicit: the API owns Prisma/PostgreSQL access and JWT authorization, while Remix handles the web experience and its server-side calls to the API.

For this small schema, decoupling is an exploration rather than a scalability claim. It shows the cost of introducing an HTTP contract even with one frontend: the deployment units can be separated, but model types and authentication behavior still have to remain aligned across both apps.

Technical architecture

The backend is a TypeScript Express service running on Node.js. Prisma maps a PostgreSQL schema containing only User, Password, and Joke. Read endpoints expose joke lists, random jokes, and individual jokes; create/delete and user endpoints use Bearer access-token middleware. The Remix frontend uses fetch to call the API and sends the access JWT in the Authorization header.

Authentication uses a two-token JWT flow. Access tokens expire after 90 minutes and refresh tokens after one year. When /auth/logout receives a valid refresh token, it stores that token in Redis under the user’s ID and sets the key to expire after 365 days. /auth/token verifies the refresh JWT and rejects refresh requests whenever that user’s blacklist key exists; /auth/login clears the key so a new login can start clean. Registration hashes passwords with bcrypt before issuing the token pair.

On the Remix side, the tokens live in a signed, HTTP-only cookie session. The app verifies the access token server-side; when it has expired, it calls /auth/token, stores the new access token, and redirects back through the authenticated route flow with the refreshed cookie.

The source also contains AWS deployment configuration for Elastic Beanstalk and RDS, Redis installation configuration, and GitHub Actions workflows for linting, typechecking, building, and branch-based staging/production deployment. Those files document a deployment target. This tutorial project is not a production product.

Monorepo vs. multirepo: type safety across the boundary

I built both layouts as a personal comparison, not as separate team implementations.

Monorepo project

The monorepo uses npm workspaces to keep the Remix frontend and Express backend under one root. The frontend imports its User and Joke model types from @prisma/client, so it can use the Prisma-generated types without a manually copied model-definition file. That removes one source of drift, but it only shares model types. It does not generate a request/response contract for the HTTP API.

Multirepo project

The multirepo layout keeps the frontend and backend as separate project trees with independent package manifests. Without a shared root node_modules, the frontend does not share the backend’s generated Prisma client, so it imports User, Password, and Joke from a checked-in prisma.d.ts copy. The UI remains typed, but schema changes now require a manual synchronization step. The README records publishing a shared npm package, using git submodules, or creating symlinks as alternative ways to distribute the types.

The practical trade-off is clear: the monorepo makes generated model types convenient to share, while the multirepo layout makes the boundary explicit at the cost of distributing the model types yourself.

What I learned

The domain is deliberately small and does not demonstrate production scale. It does provide a concrete comparison: a real Express/Remix service boundary, a Redis-backed refresh-token revocation path, and the type-distribution trade-off between workspace convenience and a separately maintained codebase.

Have a project in mind?

Tell me what you're working on and where you need help.