Skip to main content
This guide walks you through adding a custom Node.js microservice that doesn’t use gRPC/Protocol Buffers. These services are ideal for REST APIs, specialized integrations, or when you need npm packages not available in Go.

Prerequisites

  • Development environment set up with direnv allow
  • Node.js 24+ installed (provided by devenv)
  • Docker Compose or Tilt running (for testing)

Using the new-service Command

The new-service custom command scaffolds a Node.js microservice with Express.js boilerplate.
1

Generate the service scaffold

Run the command with your service name and optional port:
new-service custom <service-name> [port]
Examples:
# Generate a service on default port 8080
new-service custom custom-lang-service

# Generate a service on port 3001
new-service custom webhook-service 3001
Service names must be in kebab-case (e.g., webhook-service, not WebhookService or webhook_service).
This creates:
  • node-services/<service-name>/server.js - Express server implementation
  • node-services/<service-name>/package.json - NPM package definition
  • deploy/docker/<service-name>/Dockerfile - Production Dockerfile
  • deploy/k8s/<service-name>.nix - Kubernetes manifest module
2

Install dependencies

Navigate to your service directory and install dependencies:
cd node-services/<service-name>
npm install
Add any additional packages you need:
npm install axios
npm install --save-dev vitest supertest
3

Implement your service logic

Edit node-services/<service-name>/server.js to add your routes and business logic.Example structure:
import express from 'express';

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

app.use(express.json());

// Health check endpoint (required)
app.get('/healthz', (req, res) => {
  res.json({ status: 'ok' });
});

// Your custom routes
app.post('/api/webhook', async (req, res) => {
  try {
    const { payload } = req.body;
    // Process webhook
    res.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

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

export default app;
Refer to existing services like node-services/auth-service or node-services/custom-lang-service for complete examples.
4

Add to docker-compose.yml

Add your service to docker-compose.yml:
webhook-service:
  build:
    context: .
    dockerfile: deploy/docker/webhook-service/Dockerfile
  environment:
    PORT: "3001"
    NODE_ENV: "development"
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.webhook-service.rule=PathPrefix(`/webhook`)"
    - "traefik.http.routers.webhook-service.entrypoints=web"
    - "traefik.http.routers.webhook-service.priority=100"
    - "traefik.http.routers.webhook-service.middlewares=cors@file,rate-limit@file"
    - "traefik.http.services.webhook-service.loadbalancer.server.port=3001"
  networks:
    - app
Unlike Go services, Node.js services typically use HTTP (not h2c) and REST-style path routing.
5

Add to Kubernetes manifests (nixidy)

Add the import to deploy/nixidy/env/local.nix:
imports = [
  # ... existing imports
  ../../k8s/webhook-service.nix
];
6

Update Tiltfile

Add your service to the Tiltfile:1. Add to gen-manifests deps:
local_resource(
    'gen-manifests',
    cmd='bash scripts/gen-manifests.sh',
    deps=[
        # ... existing deps
        'deploy/k8s/webhook-service.nix',
    ],
    # ...
)
2. Add manifest collection:
manifests += find_yaml('deploy/manifests/webhook-service')
3. Add docker_build configuration:
docker_build(
    'webhook-service',
    context='.',
    dockerfile='deploy/docker/webhook-service/Dockerfile',
    live_update=[
        sync('node-services/webhook-service', '/app'),
        run('cd /app && npm install', trigger='node-services/webhook-service/package.json'),
    ],
)
4. Add k8s_resource configuration:
if manifests:
    # ... existing resources
    
    webhook_service_deps = cluster_bootstrap_deps + ['gen-manifests']
    k8s_resource('webhook-service', port_forwards=3001, resource_deps=webhook_service_deps)
7

Git add new files (CRITICAL for Nix)

CRITICAL: Nix can only reference files that are tracked in the git tree. You must run git add before generating manifests.
git add node-services/webhook-service/
git add deploy/docker/webhook-service/
git add deploy/k8s/webhook-service.nix
Without this step, gen-manifests will fail with “file not found” errors.
8

Generate manifests and test

Generate Kubernetes manifests:
gen-manifests
Start your services:
# Using Docker Compose
docker compose up webhook-service

# Using Tilt
tilt up
Test the service:
# Health check
curl http://localhost:3001/healthz

# Test your endpoint
curl -X POST http://localhost:3001/api/webhook \
  -H "Content-Type: application/json" \
  -d '{"payload": "test"}'

Adding Tests

Create a test file using Vitest:
// node-services/webhook-service/__tests__/webhook.test.js
import request from 'supertest';
import { describe, expect, it } from 'vitest';
import app from '../server.js';

describe('POST /api/webhook', () => {
  it('returns 200 with valid payload', async () => {
    const res = await request(app)
      .post('/api/webhook')
      .send({ payload: 'test' });
    expect(res.status).toBe(200);
    expect(res.body.received).toBe(true);
  });

  it('returns 400 with missing payload', async () => {
    const res = await request(app)
      .post('/api/webhook')
      .send({});
    expect(res.status).toBe(400);
  });
});
Update package.json to add test scripts:
{
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js",
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "devDependencies": {
    "vitest": "^2.0.0",
    "supertest": "^7.0.0"
  }
}
Run tests:
cd node-services/webhook-service
npm test

Environment Variables

Add environment variables in your service configuration: In docker-compose.yml:
webhook-service:
  environment:
    PORT: "3001"
    NODE_ENV: "development"
    API_KEY: "${WEBHOOK_API_KEY}"
    DATABASE_URL: "${DATABASE_URL}"
In your service code:
const API_KEY = process.env.API_KEY;
if (!API_KEY) {
  throw new Error('API_KEY environment variable is required');
}

Common Issues

You forgot to git add the new files. Nix requires all files to be in the git tree.
git add deploy/k8s/webhook-service.nix
git add node-services/webhook-service/
Check your Dockerfile has the correct Node.js version and working directory setup:
FROM node:24-alpine
WORKDIR /app
COPY node-services/webhook-service/package*.json ./
RUN npm ci --only=production
COPY node-services/webhook-service/ .
CMD ["node", "server.js"]
Verify the PathPrefix rule in docker-compose.yml:
- "traefik.http.routers.webhook-service.rule=PathPrefix(`/webhook`)"
Test directly first without Traefik:
curl http://localhost:3001/webhook
Ensure live_update configuration is correct in Tiltfile:
live_update=[
    sync('node-services/webhook-service', '/app'),
    run('cd /app && npm install', trigger='package.json'),
]

Next Steps

  • Add comprehensive tests with Vitest
  • Implement logging and error handling
  • Add request validation middleware
  • Configure CORS and rate limiting

See Also

Build docs developers (and LLMs) love