Next.js β NHS Quickstart
When you need a fast, secure UI with server-side rendering (SSR) and built-in API routes, Next.js keeps everything in one codebase. You can fetch from internal APIs (e.g., FastAPI) server-side so secrets never reach the browser.
Great for: Developer Β· Data Engineer (ops tools) Β· IGβsafe intranet apps.
βοΈ 10-minute installβ
npx create-next-app@latest nhs-next --ts --eslint
cd nhs-next
npm run dev
You now have the App Router (/app) structure.
π βHello NHSβ β pages + API routeβ
- A) API Route (app/api/kpi/route.ts)
- B) Server Component page (app/page.tsx)
- C) Env & secrets (.env.local)
Create a route handler that returns a small JSON payload. In production, call your internal FastAPI service or read from Parquet.
import { NextResponse } from 'next/server'
export async function GET() {
// In production, fetch from your FastAPI URL with server-side secrets.
const rows = [
{ practice_id: 'A86005', total_appointments: 42, attendance_rate: 0.92, median_wait_minutes: 12 }
]
return NextResponse.json({ rows })
}
Fetch from the route handler with cache: 'no-store' for live KPIs.
async function getData() {
const res = await fetch('http://localhost:3000/api/kpi', { cache: 'no-store' })
if (!res.ok) throw new Error('Failed to fetch KPI')
return res.json() as Promise<{ rows: { practice_id:string; total_appointments:number; attendance_rate:number; median_wait_minutes:number }[] }>
}
export default async function Home() {
const data = await getData()
return (
<main style={{ padding: 16 }}>
<h2>NHS KPI (SSR)</h2>
<pre>{JSON.stringify(data.rows, null, 2)}</pre>
</main>
)
}
Put secrets in .env.local (gitignored). Never expose server secrets via NEXT_PUBLIC_ unless safe.
API_BASE_URL=http://localhost:8000 # e.g., your FastAPI base
π Proxy to FastAPI (recommended)β
Keep SQL connectivity in FastAPI; Next.js calls the API server-side.
import { NextResponse } from 'next/server'
export async function GET() {
const base = process.env.API_BASE_URL!
const r = await fetch(`${base}/kpi`, { cache: 'no-store' })
const data = await r.json()
return NextResponse.json(data)
}
This keeps DB drivers off the Node side and centralises audit/validation in one service.
π Auth (Azure AD / Entra ID with next-auth)β
- Install
- Route + provider
- Protect a page
Install next-auth and an Azure AD provider.
npm install next-auth @auth/core
Create the auth route and Azure AD provider config. Replace placeholders with your tenant and client IDs.
import NextAuth from "next-auth"
import AzureAD from "@auth/core/providers/azure-ad"
import { authConfig } from "@/auth.config"
const handler = NextAuth({
...authConfig,
providers: [
AzureAD({
clientId: process.env.AZURE_AD_CLIENT_ID!,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
tenantId: process.env.AZURE_AD_TENANT_ID!,
})
],
session: { strategy: "jwt" }
})
export { handler as GET, handler as POST }
Use getServerSession to guard server components.
import { getServerSession } from "next-auth"
export default async function ProtectedPage() {
const session = await getServerSession()
if (!session) {
return <p>Sign-in required</p>
}
return <div>Protected content for {session.user?.name}</div>
}
Set the following in .env.local:
AZURE_AD_TENANT_ID=YOUR_TENANT_ID
AZURE_AD_CLIENT_ID=YOUR_APP_CLIENT_ID
AZURE_AD_CLIENT_SECRET=YOUR_APP_CLIENT_SECRET
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=change_me
π¦ Styling & NHS brandingβ
- Use @nhsuk/frontend CSS via a simple import (global).
- Or a lightweight Tailwind setup for internal tools.
- Add a βData last updatedβ footer based on the API response timestamp.
Global CSS import example
import "./globals.css"
import "@nhsuk/frontend/dist/nhsuk.css"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="nhsuk-width-container">{children}</body>
</html>
)
}
π³ Docker (production-like)β
Dockerfile
# Install deps
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
RUN npm ci || yarn install --frozen-lockfile || pnpm install --frozen-lockfile
# Build
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Run
FROM node:20-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/public ./public
COPY --from=build /app/.next ./.next
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./package.json
EXPOSE 3000
CMD ["npm", "start"]
Build and run:
docker build -t nhs-next .
docker run -p 3000:3000 --env-file .env.local nhs-next
Deploy to AWS App Runner or Azure App Service/Container Apps; keep private networking and front with SSO/WAF as required.
π Performance & cachingβ
- Live KPIs:
fetch(..., { cache: 'no-store' }). - Cached pages:
export const revalidate = 300in a page to ISR every 5 minutes. - Use server actions or route handlers for computations; avoid shipping secrets/client keys.
π‘ IG & safety checklistβ
- Keep secrets server-side; never expose DB creds in browser JS.
- Prefer API β SSR pattern: UI only calls your own API; API calls the DB/warehouse.
- Add small-number suppression in the API; render read-only KPIs.
- Log access at the proxy and API layers; keep an audit README in the repo.
- Disable
/apipublic access if app is intranet-only (private networking, auth).
π Measuring impactβ
- Time to interactive (TTI) for core pages.
- p95 API latency when fetching KPIs server-side.
- Cache efficiency: ISR hit rate; revalidate frequency.
- Security: endpoints behind auth; secrets in store not code.
π See alsoβ
Whatβs next?
Youβve completed the Learn β Next.js stage. Keep momentum: