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
package.jsonandpackage-lock.jsoncommitted at the repository root- A
startscript (or the full command) that starts the server - GitHub account connected to Zenifra
Listening on the right port
Read the port from a variable with a default value and listen on 0.0.0.0:
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
| Project | build | start |
|---|---|---|
| Express or Fastify in JavaScript | empty | node index.js |
| Express or Fastify in TypeScript | npm run build | node dist/index.js |
| NestJS | npm run build | node 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:
{
"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.jsThe 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
| Symptom | Fix |
|---|---|
Error: listen EADDRINUSE | Two servers on the same port. Start only one process in start |
| Public URL does not respond, no errors in logs | Server 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 browser | Add the front-end domain to the allowed CORS origins |
Next steps
Last updated on
Deploy Next.js on Zenifra
Deploy a Next.js app from GitHub with build and start commands, NEXT_PUBLIC variables, a health check, a database, and fixes for the most common errors.
Static sites and SPAs (Vite, React, Vue, Astro)
Deploy static sites and SPAs built with Vite, React, Vue, Angular, or Astro on Zenifra, using the Node.js runtime or the official NGINX image.