Framework guides

Node.js APIs with Express, Fastify, and NestJS

This guide covers Node.js APIs and backends deployed from a GitHub Repository. Zenifra installs dependencies with npm ci, runs the build when configured, and starts the app with the start command.

Prerequisites

Listening on the right port

Read the port from a variable with a default value and listen on 0.0.0.0:

index.js
const express = require('express');

const app = express();
app.use(express.json());

app.get('/health', (req, res) => res.json({ status: 'ok' }));
app.get('/', (req, res) => res.json({ message: 'Hello from Zenifra' }));

const port = Number(process.env.PORT ?? 3000);
app.listen(port, '0.0.0.0', () => console.log(`Listening on port ${port}`));

The project Port field must have the same value. If you do not define a PORT variable, the code's default 3000 is used.

Console values

Projectbuildstart
Express or Fastify in JavaScriptemptynode index.js
Express or Fastify in TypeScriptnpm run buildnode dist/index.js
NestJSnpm run buildnode dist/main.js

Use Port 3000 in all three cases, or whichever port the code uses.

No build, no devDependencies

When the build field is empty, devDependencies are removed before the app starts. Everything start uses at runtime must be in dependencies. Do not use ts-node, tsx, or nodemon in a production start: compile during the build and run the generated JavaScript.

NestJS

NestJS compiles to dist/ with nest build. Check the script:

package.json
{
  "scripts": {
    "build": "nest build",
    "start:prod": "node dist/main.js"
  }
}

@nestjs/cli can stay in devDependencies, because the build runs with them installed when the build field is filled in.

For a health check with dependencies, the @nestjs/terminus package provides a ready-made route. Keep it fast: the health check runs every minute and restarts the instance on failure.

Migrations

Project variables, including DATABASE_URL, exist both during the build and at runtime. The most predictable approach is to apply migrations at the beginning of start:

start: npx prisma migrate deploy && node dist/main.js

The tool used in start must be installed at runtime: in dependencies, or in devDependencies when the build field is filled in.

Graceful shutdown

On every deployment or restart, the old instance receives SIGTERM. Close connections and finish in-flight requests:

process.on('SIGTERM', () => {
  server.close(() => process.exit(0));
});

In NestJS, app.enableShutdownHooks() does this for you.

WebSockets and Server-Sent Events

WebSocket and SSE connections work over the project's HTTP port, including on custom domains. With multiple instances, each connection lives on one instance: to broadcast to every client, use a shared channel such as the managed cache with pub/sub.

Workers and queues

Background processes, such as queue consumers, can run as a separate project with their own start command. See the Python worker example and managed queues. If you want a health check on a worker, expose a minimal /health route on the project port.

Common problems

SymptomFix
Error: listen EADDRINUSETwo servers on the same port. Start only one process in start
Public URL does not respond, no errors in logsServer listening on localhost. Use 0.0.0.0
Cannot find module 'dist/main.js'Empty build field or a different outDir in tsconfig.json
Cannot find module 'ts-node'Compile during the build and run JavaScript
CORS error in the browserAdd the front-end domain to the allowed CORS origins

Next steps

Last updated on

On this page