Deploying a Remix app to AWS with CDK

The main image for this blog post

Remix is a full stack web framework, built on the Web-Fetch API, that emphasises user experience and web standards. AWS is a Amazon’s cloud Infrastructure as a Service (IaaS) / Platform as a Service (PaaS) used to build scalable, modular applications on the cloud, and the AWS Cloud Development Kit (CDK) is an open-source framework for defining infrastructure in code and provisioning it through AWS.

In this guide we will be combining these technologies to build a modern, efficient, and scalable full stack application. Before we start putting everything together though, there are a few considerations we first need to cover.

Deployment Considerations

Let’s talk about what we’re actually deploying. Something important to understand about Remix is that it’s not really an HTTP server - it’s a handler inside an existing HTTP server, which uses adapters to process requests between the server request/response handler and Remix’s request/response handler.

According to the docs, deploying a Remix application has four layers:

  • JavaScript runtime
  • Web server
  • Server adapter
  • Host/platform.

Here’s what we’ll be using for our application:

  • Runtime: NodeJS
  • Web Server: Express (used by the default remix-server)
  • Server Adapter: @remix-run/architect
  • Host: AWS Lambda

What's an Adapter?

Just to quickly explain what an adapter is; HTTP servers have a their own Request/Response API. An adapter is used to convert the incoming server request to a Web Fetch Request, run the Remix handler, and then adapt the Remix handlers Web Fetch Response back to the host servers response API.

Diagram to show the flow of data between the server and adapter.

Remix’s Build Output

Remix runs your app on the server as well as in the browser. However, it doesn’t run all of your code in both places. During the build step, the compiler splits your project into two build folders:

  1. build/server/index.js - A server HTTP handler that includes all routes and modules together to be able to render on the server and handle any other server-side requests for resources.
  2. build/client/* - A client build that includes automatic code splitting by route, fingerprinted asset imports (like CSS and images etc.). Anything needed to run an application in the browser.

Given these outputs, your deployment architecture needs to account for both server-side and client-side code. Specifically, you’ll need a place to run the server code and another to host the static assets for the client side. In the next section, we’ll discuss how to achieve this on AWS using services like S3, Lambda, and CloudFront.

Architecture

How are we going to put all of this together on AWS?

In our AWS architecture, we’re going to need to have somewhere to host our static asset files, somewhere from which to serve our server side code, and a CDN to cache and serve both static assets and dynamic content efficiently. Here are the AWS services we’ll use to achieve this:

  • CloudFront can act as our CDN.
  • S3 Bucket can store our client build files and static assets (e.g., JavaScript, CSS, images).
  • API Gateway can route all non asset requests to a lambda function.
  • Lambda function can run the server-side code for handling SSR using the Remix framework.

Here’s a basic diagram of what this architecture might look like:

Diagram demonstrating the basic architecture for this Remix / AWS deployment.

Project Set Up

Folder Structure

Using CDK, we can write our own construct to define all of this infrastructure as code. We can then extend this construct in our Remix application which will then be added to a stack that we can deploy to AWS.

Using a monorepo project structure provides a good balance of modularity, maintainability, and efficiency, while keeping everything within a single repository for ease of management. There are a few different options for setting up monorepo projects, I’ve gone for pnpm.

You’ll want your monorepo to look like this. If you’re using a different tool for managing your monorepo then your setup might look a little different:

Set up your CDK App

Set up your Remix App

Set up your Remix Construct

Set up your root workspace files

Define the Stack

Now is a good time to synthesise and deploy our stack, there’s nothing on it yet, but we can sign in to the AWS console and see that the stack exists.

Remix App Setup

We’ve initialised our Remix application and have a standard ‘hello world’ type Remix application. There are a few changes we now need to make so that our lambda function will be able to execute this code.

Module package types

By default, the Remix app will have "type":"module" in its package.json, which will cause some problems when our lambda function attempts to import code from this project. To get around this we need to do the following:

  • Remove "type":"module" from the package.json of the Remix project
  • Rename postcss.config.js to postcssconfig.mjs
  • Rename vite.config.ts to vite.config.mts

Setting up the server file

Earlier we discussed adapters, which are used to convert request/response requests from your server to the Web Fetch API and back again. We need to use an adapter that will take requests from our AWS lambda function and process those for Remix.

We need to create a server.ts file at the root of our Remix project and export a handler, this will be the ultimate entrypoint to our Remix code for our lambda function.

Fortunately, we can use one of the adapters maintained by the Remix team (@remix-run/architect) which we will then export as the handler function.

Building the Infrastructure

Cool, so at this point we’ve got our monorepo set up, we’ve got a basic Remix project set up, and we’ve got an empty stack deploying to AWS - Let’s now start building the infrastructure. S3 Buckets are fairly simple to set up, so lets start by creating a simple construct we’ll call RemixSite, consisting of an S3 Bucket. Then we will create an instance of our construct in the Remix app, and add that to our stack.

Create the RemixSite Construct

Extend our new RemixSite construct in the infrastructure folder of our Remix application

Add the extended construct to our stack

Now when you run cdk synth and cdk deploy in your main cdk app directory (/app), you should see that a new S3 Bucket resource has been added to your stack when you go in to your AWS console. The stack we’re deploying exists under /app which is consuming a RemixApp construct from our Remix application, which is consuming a RemixSite to define its serverless architecture.

Building the Lambda Server Function

The server code that we are building from our Remix application will be executed by a lambda function and fronted by an API Gateway. We’ll build a lambda function that uses the handler exported from the server.ts file we made earlier and executes the code in our /build folder.

NodeJsFunction uses esbuild to compile your lambda code. There are a few bundling options we will need to add so that we can use the Vite build of Remix:

  • format - We need to set this to use ESM as that’s the module format used by the server.ts file in our Remix application (i.e. export const handler ...). Without this we’ll get some errors, when we try to synthesise our app, about trying to use a top-level await as this isn’t compatible with CJS (NodeJsFunction's default).
  • banner - In ESM, we don’t have access to Node’s internal modules (like fs, path, etc.), these also can’t be bundled by esbuild. This means that when we try to visit our website we’ll get an “internal server error”, and in our CloudWatch logs we’ll see an error along the lines of Dynamic require of \\"node: ____ \\" is not supported. We can get around this by using banner to use createRequire at the top of our entry file, which will allow us to create a require function that can be used within ESM code.
  • nodeModules - This describes specific modules will be installed directly into the Lambda function's deployment package. Which is useful for modules that are not compatible with esbuild.

Build the CloudFront Distribution

If you look back to our initial diagram, we send all requests for /assets to our S3 Bucket and requests for all other routes through to our lambda function. Here’s where we make that happen, by defining our additionalBehaviours and defaultBehaviours.

If you synthesise and deploy your app now you’ll see that your server code is deploying and you can see your website in your browser. You’ll also notice that there isn’t any styling, assets or client side javascript. That’s because we still need to deploy our bucket and set it up as an origin for our distribution so that requests to /assets are able to retrieve our client side code.

And here’s our finished RemixSite Construct:

Now, if you visit your site you’ll see it has a fresh new look - this is because we’re now sending successful requests to /assets and receiving all of our client side code.

Conclusion

By integrating Remix with AWS Lambda, S3, CloudFront, and API Gateway, you can achieve server-side rendering, efficient content delivery, and static asset hosting.

With AWS CDK’s infrastructure-as-code capabilities, this setup remains flexible and easy to manage, making it easier to adapt to future needs and scale your application as it grows.

By following this guide, you should now have a good understanding of how to deploy a Remix app on AWS, combining the best of serverless and full-stack technologies. Happy deploying!