Next.js web application that provides the UI for the TODO app, with built-in OpenTelemetry instrumentation.
graph LR
Browser[Browser] -->|HTTP| Next[Next.js Server]
Next -->|SSR| Page[Server Component<br/>page.tsx]
Page -->|fetch| Backend[Backend API]
Browser -->|Client Actions| API[API Route<br/>/api/todos]
API -->|fetch| Backend
Next --> OTel[OpenTelemetry SDK]
OTel -->|OTLP HTTP| Collector[OTel Collector]
The frontend is built with Next.js and React (TypeScript). It uses a hybrid rendering strategy:
- Server-side: The main page (
page.tsx) is a React Server Component that fetches todos from the backend at request time and renders the initial HTML. - Client-side: Interactive operations (create, toggle, delete) are handled by client components (
TodoList,AddTodo,TodoItem) that call the frontend's own API proxy routes.
| File / Directory | Purpose |
|---|---|
src/app/page.tsx |
Server component – fetches and renders the todo dashboard |
src/app/api/todos/route.ts |
API proxy – forwards client requests to the backend with tracing |
src/components/TodoList.tsx |
Client component – manages todo state and user interactions |
src/components/TodoItem.tsx |
Client component – renders a single todo row |
src/components/AddTodo.tsx |
Client component – form for creating new todos |
src/instrumentation.ts |
OpenTelemetry SDK initialization (Next.js instrumentation hook) |
next.config.ts |
Next.js configuration (output: "standalone") |
sequenceDiagram
participant B as Browser
participant F as Frontend (Next.js)
participant BE as Backend (Express)
participant R as Redis
B->>F: GET / (page load)
F->>BE: GET ${BACKEND_URL}/todos
BE->>R: KEYS todo:*
R-->>BE: keys
BE-->>F: JSON array
F-->>B: Rendered HTML
B->>F: POST /api/todos
F->>BE: POST ${BACKEND_URL}/todos
BE->>R: HSET todo:{id}
R-->>BE: OK
BE-->>F: 201 Created
F-->>B: JSON response
The frontend connects to the backend through the BACKEND_URL environment variable, which must be set (the app will crash on startup without it). This URL should include the base path if one is configured on the backend (e.g. http://traefik/api).
Two connection paths exist:
- Server-side rendering –
page.tsxcallsfetch("${BACKEND_URL}/todos")directly during SSR. - Client-side actions – Client components call
/api/todoson the frontend, which proxies the request to${BACKEND_URL}/todoson the backend.
Configuration is done via environment variables.
| Variable | Default | Description |
|---|---|---|
BACKEND_URL |
(none – required) | Backend API base URL including any base path (e.g. http://traefik/api) |
OTEL_SERVICE_NAME |
todo-frontend |
Service name reported to the OTel collector |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4318 |
OTLP HTTP endpoint for traces, metrics, and logs |
OTEL_EXPORTER_OTLP_PROTOCOL |
— | Protocol (http/protobuf in docker-compose) |
OTEL_TRACES_EXPORTER |
— | Traces exporter type (otlp) |
OTEL_METRICS_EXPORTER |
— | Metrics exporter type (otlp) |
OTEL_LOGS_EXPORTER |
— | Logs exporter type (otlp) |
NEXT_RUNTIME |
— | Must be set to nodejs for OpenTelemetry to activate |
flowchart TD
subgraph Frontend Process
Hook[instrumentation.ts<br/>register hook]
SSR[Server Components]
APIRoute[API Route Handlers]
end
Hook -->|Traces| Collector[OTel Collector]
Hook -->|Metrics| Collector
Hook -->|Logs| Collector
APIRoute -->|Custom Spans| Hook
OpenTelemetry is integrated using Next.js's instrumentation.ts hook. The register() function is called automatically by Next.js at startup. It only initializes the SDK when NEXT_RUNTIME equals nodejs (skipped in Edge runtime and during build).
- Auto-instrumentation for outbound HTTP requests is enabled.
- Manual spans are created in the API proxy (
src/app/api/todos/route.ts) for each operation:frontend.todos.list– listing todosfrontend.todos.create– creating a todofrontend.todos.toggle– toggling checked statefrontend.todos.delete– deleting a todo
Each span records relevant attributes (todo.id, todo.title, todo.count) and captures exceptions on failure.
Metrics and logs are exported via OTLP HTTP with a 10-second export interval, matching the backend configuration.
The Dockerfile uses a multi-stage build with Node 25 Alpine and Next.js standalone output.
# Build the image
docker build -t todo-frontend ./frontend
# Run the container
docker run -p 3000:3000 \
-e BACKEND_URL=http://host.docker.internal:4000 \
todo-frontend| Stage | What happens |
|---|---|
| builder | Installs dependencies (npm ci), runs next build producing a standalone bundle |
| runtime | Copies the .next/standalone output and static assets, exposes port 3000, starts with node server.js |
The standalone output mode (configured in next.config.ts) bundles all dependencies into a self-contained directory, resulting in a smaller production image.
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Start production build
npm start