CI/CD for a Node/Express API with GitHub Actions
A Node/Express API needs a pipeline that installs deps, lints, runs tests against a real database, and ships on green.
This recipe gives you a working GitHub Actions workflow for an Express API. It runs unit and integration tests against a Postgres service container, then deploys on a push to main. Adapt the deploy step to your host.
What the pipeline does
- Install dependencies with npm ci
- Lint with ESLint
- Run tests against a Postgres service
- Build (transpile/bundle if needed)
- Deploy on push to main
The workflow
Two jobs: a test job with a Postgres service container, and a deploy job gated on the default branch.
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_test
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- run: npm ci --omit=dev
- run: npm run deployTesting against a database
The postgres service runs in a sidecar container reachable at localhost:5432 from the job. The health check ensures the database is accepting connections before your tests start, so point your test config at DATABASE_URL and run migrations in a beforeAll or a dedicated step. Most registry pulls and network calls here are transient; managed runners that auto-retry these flakes keep the job green without a manual re-run, and Latchkey is about 69% cheaper than GitHub-hosted runners.
Deploying
The deploy job runs only on main. Swap npm run deploy for your target: a platform CLI (Fly, Render, Railway), an SSH rsync to a VPS, or a container push to ECS. Store credentials in repository secrets and reference them as environment variables in the deploy step.
Key takeaways
- Run integration tests against a Postgres service container, not a mock.
- Gate the deploy job on github.ref == refs/heads/main and needs: test.
- Cache npm via setup-node to cut install time on every run.