Skip to main content

Express.js β€” NHS Quickstart

🟨 Node.js · REST APIs · SQL Server/Postgres · Simple, fast, reliable
Why Express for the NHS

If your team is JavaScript-first, Express is a tiny, reliable way to wrap SQL views and micro-tools with REST endpoints. Keep logic server-side, ship OpenAPI-lite endpoints, and front them with SSO.

Great for: Developer Β· Data Engineer (utility APIs) Β· Ops tools backing dashboards.


βš™οΈ 10-minute install​

mkdir nhs-express && cd nhs-express
npm init -y
npm i express mssql dotenv cors helmet pino-http

Project layout

nhs-express/
.env
index.js
package.json

.env (local only β€” never commit)

SQLSERVER_SERVER=localhost
SQLSERVER_DATABASE=NHS_Analytics
PORT=3000

πŸš€ β€œHello NHS” API β€” read from a view​

Create index.js:

index.js
const express = require('express')
const sql = require('mssql')
const cors = require('cors')
const helmet = require('helmet')
const pino = require('pino-http')()
require('dotenv').config()

const app = express()
app.use(helmet())
app.use(cors({ origin: false })) // adjust when calling from a browser origin
app.use(pino)

// Global connection pool (reused across requests)
const pool = new sql.ConnectionPool({
server: process.env.SQLSERVER_SERVER,
database: process.env.SQLSERVER_DATABASE,
options: { encrypt: true, trustServerCertificate: true }, // per Trust policy
authentication: { type: 'ntlm' } // or 'default' with user/password if needed
})

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

// Read-only KPI endpoint
app.get('/kpi', async (req, res) => {
try {
await pool.connect()
const r = await pool.request().query(`
SELECT TOP (50)
practice_id, total_appointments, attendance_rate, median_wait_minutes
FROM dbo.vw_PracticeKPI
ORDER BY total_appointments DESC
`)
res.json(r.recordset)
} catch (err) {
req.log.error(err)
res.status(500).json({ error: 'query_failed' })
}
})

const port = process.env.PORT || 3000
app.listen(port, () => console.log(`http://localhost:${port}`))

Run:

node index.js

πŸ” Security patterns​

Use a shared header for development, then swap to Entra ID for production.

// apiKey.js
module.exports = function requireApiKey(req, res, next) {
const ok = req.header('x-api-key') && req.header('x-api-key') === process.env.API_KEY
if (!ok) return res.status(401).json({ error: 'unauthorised' })
next()
}

// usage:
// app.get('/kpi', requireApiKey, async (req,res)=>{ ... })

CORS

// allow only your intranet origin (if a browser UI calls this API)
app.use(cors({
origin: ['https://intranet.yourtrust.nhs'],
methods: ['GET','POST'],
allowedHeaders: ['Content-Type','Authorization','x-api-key']
}))

🐳 Docker (production-like)​

Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV PORT=3000
EXPOSE 3000
CMD ["node", "index.js"]

Build & run:

docker build -t nhs-express .
docker run -p 3000:3000 --env-file .env nhs-express

Deploy to AWS App Runner or Azure App Service/Container Apps; keep networking private and front with SSO/WAF as required.


πŸ”Ž Observability & resilience​

  • Logs: pino-http outputs JSON; ship to CloudWatch / Log Analytics.
  • Health: /healthz endpoint; add DB ping if needed.
  • Shutdown: listen for SIGTERM and close the pool/server gracefully.
  • Validation: whitelist/validate query params (e.g., with zod or joi).

πŸ”’ IG & safety checklist​

  • Use read-only DB roles for GET endpoints; separate accounts for writes.
  • Apply small-number suppression for aggregates.
  • Keep secrets in a secret store (Key Vault/Secrets Manager); never commit .env.
  • Restrict CORS to known origins; protect /docs or admin routes behind auth.
  • Record data usage and endpoint owners in the repo README.

πŸ“ Measuring impact​

  • Latency: p95 for /kpi.
  • Reliability: 5xx rate, uptime.
  • Security: % endpoints protected; secrets from store.
  • Adoption: number of dashboards/services calling the API.

πŸ”— See also​

See also: FastAPI Β· Next.js Β· Docker Β· AWS Β· Azure Β· Secrets & .env Β· SQL

What’s next?

You’ve completed the Learn β€” Express.js stage. Keep momentum: