Skip to content


Let's deploy the todo app to

Prepare for Production

In this tutorial, we'll deploy both the React app and the API server as one server-side app, and redirect all non-API requests to return the React app.

We will deploy an ESM node server project

In addition, to follow a few basic production best practices, we'll use compression middleware to improve performance and helmet middleware for security

  1. Install compression and helmet.
npm i compression helmet
npm i @types/compression --save-dev
  1. Add the highlighted code lines to src/server/index.ts, and modify the app.listen function's port argument to prefer a port number provided by the production host's PORT environment variable.
// src/server/index.ts

import express from "express"
import { api } from "./api.js"
import session from "cookie-session"
import { auth } from "./auth.js"
import helmet from "helmet"
import compression from "compression"

const app = express()
    secret: process.env["SESSION_SECRET"] || "my secret"
const frontendFiles = process.cwd() + "/dist";
app.get("/*", (_, res) => {
  res.sendFile(frontendFiles + "/index.html");
app.listen(process.env["PORT"] || 3002, () => console.log("Server started"));
  1. Modify the highlighted code in the api server module to prefer a connectionString provided by the production host's DATABASE_URL environment variable.

    // src/server/api.ts
    const DATABASE_URL = process.env["DATABASE_URL"];
    export const api = remultExpress({
     dataProvider: DATABASE_URL
       ? createPostgresDataProvider({ connectionString: DATABASE_URL })
       : undefined,


In order to connect to a local PostgresDB, add DATABASE_URL to an .env file, or simply replace process.env["DATABASE_URL"] with your connectionString.

If no DATABASE_URL has found, it'll fallback to our local JSON files.

  1. In the root folder, create a TypeScript configuration file tsconfig.server.json for the build of the server project using TypeScript.
// tsconfig.server.json

  "compilerOptions": {
    "experimentalDecorators": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "outDir": "dist",
    "rootDir": "src",
    "module": "nodenext"
  "include": ["src/server/**/*", "src/shared/**/*"]
  1. Modify the project's build npm script to additionally transpile the API server's TypeScript code to JavaScript (using tsc).
// package.json

"build": "tsc && vite build && tsc -p tsconfig.server.json",
  1. Modify the project's start npm script to start the production Node.js server.
// package.json

"start": "node dist/server/"

The todo app is now ready for deployment to production.

Test Locally

To test the application locally run

npm run build
npm run start

Build Errors

If you get an error error TS5096: Option 'allowImportingTsExtensions' can only be used when either 'noEmit' or 'emitDeclarationOnly' is set. do not set the emitDeclarationOnly flag!

You are getting the error because somewhere in your code you've imported from .ts instead of .js - fix it and build again

Now navigate to http://localhost:3002 and test the application locally

Deploy to Railway

In order to deploy the todo app to railway you'll need a railway account. You'll also need Railway CLI installed, and you'll need to login to railway from the cli, using railway login.

Click enter multiple times to answer all its questions with the default answer

  1. Create a Railway project.

    From the terminal in your project folder run:

    railway init
  2. Select Empty Project

  3. Set a project name.

  4. Once it's done add a database by running the following command:

    railway add
  5. Select postgressql as the database.

  6. Once that's done run the following command to upload the project to railway:

    railway up
  7. got to the railway project's site and click on the project

  8. Switch to the variables tab

  9. Click on + New Variable, and in the VARIABLE_NAME click Add Reference and select DATABASE_URL

  10. Add another variable called SESSION_SECRET and set it to a random string, you can use an online UUID generator

  11. Switch to the settings tab

  12. Under Environment click on Generate Domain

  13. Click on the newly generated url to open the app in the browser and you'll see the app live in production. (it may take a few minutes to go live)


If you run into trouble deploying the app to Railway, try using Railway's documentation.

That's it - our application is deployed to production, play with it and enjoy.

To see a larger more complex code base, visit our CRM example project

Love Remult?  Give our repo a star.⭐

MIT Licensed | Made by the Remult team with ❤️