Deploy an Express API

Deploy an Express API on Lucity. Three requirements, none of them platform-specific.

Express has no build step, no framework conventions and no opinions about deployment, which means the platform has less to detect. Three things make an Express app deployable here, and all three are things you would want anyway.

One: a start script

Detection looks for a start script in package.json, then a main field, then index.js. A project with none of those produces no start command, and the service will build but never run.

{
  "scripts": {
    "start": "node index.js"
  }
}

Two: listen on the port you are given

Lucity sets PORT in the container. Read it, and keep a local default so npm run dev still works:

import express from 'express';

const app = express();
const port = process.env.PORT || 3000;

app.get('/', (request, response) => {
  response.json({ message: 'Deployed on Lucity' });
});

app.listen(port, () => console.log(`listening on ${port}`));

A hardcoded port is the single most common reason an Express service never becomes healthy: the process runs, the platform checks the port it assigned, nothing answers.

Three: a health check that means something

By default the platform opens a TCP connection to your port and calls that healthy. That catches a crashed process, but not an app that is up and failing every request.

Add a route that answers cheaply, then point the service's health check at it in service settings:

app.get('/healthz', (request, response) => response.status(200).send('ok'));

Keep it honest but light. Checking the database on every probe turns a slow query into a restart loop; checking nothing at all means a broken app stays in rotation.

zeitlos/express-example(opens in a new tab) is these three things and nothing else.

Connecting a database

Add a PostgreSQL database and link DATABASE_URL to its fqdn-uri:

import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

One pool per process, created at startup, not per request. With more than one replica, remember that each has its own pool: the connection limit that matters is pool size times replicas.

Sessions and rate limiting want a key-value store rather than in-memory state, for the same reason. Anything held in memory belongs to one replica and disappears when it rolls.

Migrations

There is no framework convention here, so pick one and put it in the start script:

{
  "scripts": {
    "start": "node migrate.js && node index.js"
  }
}

Running with every start means every replica attempts it, so the tool you use has to be safe to run concurrently. Most are; check yours.

Next steps

  • Services for health checks, resources and replicas
  • Key-value store for sessions, caching and queues
  • Logs for what your app prints in production