A Go service that extracts structured health metrics from free-text clinical notes. Given a block of consultation text, the API identifies and normalises weight and height measurements into a consistent JSON response.
docker compose up --buildgo run ./cmd/serverThe server starts on port 8080 by default.
All configuration is read from environment variables with sensible defaults.
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
HTTP listen port |
READ_TIMEOUT |
10s |
Maximum duration for reading request |
WRITE_TIMEOUT |
10s |
Maximum duration for writing response |
IDLE_TIMEOUT |
60s |
Maximum duration for keep-alive |
SHUTDOWN_TIMEOUT |
15s |
Graceful shutdown drain period |
MAX_BODY_BYTES |
1048576 |
Maximum request body size (1 MB) |
MAX_TEXT_LENGTH |
10000 |
Maximum text field length (chars) |
Accepts clinical free text and returns any health metrics found.
Request
curl -X POST http://localhost:8080/parse \
-H "Content-Type: application/json" \
-d '{
"text": "Patient attended the clinic today with a pain in their left knee. Observations taken at the time were a weight of 9st 3lbs and height of 180cm tall. On examination there was a good range in movement and slight tenderness. Advised to use OTC ibuprofen gel and rest for 7 days."
}'Response
{
"weight": "58.5kg",
"height": "180cm"
}Fields are omitted from the response when the corresponding metric is not found in the text.
Supported metrics
| Metric | Recognised formats | Normalised to |
|---|---|---|
| Weight | 75kg, 9st 3lbs, 165lbs, 80 kilograms |
kg |
| Height | 180cm, 1.80m, 5ft 11in, 5'11" |
cm |
Error responses
| Status | Meaning | Example body |
|---|---|---|
| 400 | Missing or empty text field |
{"error":"text field is required and must not be empty"} |
| 400 | Malformed JSON | {"error":"invalid JSON: ..."} |
| 400 | Unknown fields in request | {"error":"invalid JSON: ..."} |
| 400 | Text exceeds max length | {"error":"text exceeds maximum length"} |
| 415 | Wrong Content-Type |
{"error":"Content-Type must be application/json"} |
Returns {"status":"ok"} for use by container orchestrators and load balancers.
The project has three layers of tests (testing pyramid):
- Unit tests — each extractor tested in isolation with table-driven tests
- Handler tests — HTTP layer tested with both real parser and mock parser
- Integration tests — full HTTP stack (router → middleware → handler → parser)
go test ./... -race -count=1
# Verbose
go test ./... -race -count=1 -v
# Benchmarks
go test ./internal/parser -bench=. -benchmem
# Lint (requires golangci-lint)
golangci-lint run ./...This project applies five recognised design patterns, each chosen for a specific architectural benefit:
Each metric type (weight, height) is extracted by a separate struct that implements the MetricExtractor interface:
type MetricExtractor interface {
Extract(text string) (value string, found bool)
}The Parser delegates to its extractors without knowing their internals. This means adding a new metric (e.g. blood pressure) requires only writing a new struct that implements Extract — zero changes to existing code. This is the Open/Closed Principle in practice.
The handler defines a local Parser interface for what it needs:
// in handler package — no import of parser package
type Parser interface {
Parse(text string) model.ParseResponse
}The concrete *parser.Parser satisfies this interface implicitly. The handler package never imports the parser package — it depends only on the behaviour it needs. This is the canonical Go pattern: accept interfaces, return structs.
The handler uses the functional options pattern for extensible configuration:
h := handler.New(p,
handler.WithMaxTextLength(cfg.MaxTextLength),
)This pattern scales without breaking existing call sites. Adding a new option (e.g. WithLogger) only requires a new With* function — no constructor signature changes, no config struct versioning.
Middleware functions wrap the HTTP handler with cross-cutting concerns:
app := middleware.Chain(mux,
middleware.Recovery, // outermost — catches panics
middleware.SecureHeaders, // X-Content-Type-Options, X-Frame-Options
middleware.Logger, // logs every request
middleware.MaxBodySize(cfg.MaxBodyBytes),
)Each middleware is a func(http.Handler) http.Handler that decorates the next handler. Chain composes them in order. This is both the Decorator pattern (wrapping behaviour) and Chain of Responsibility (each middleware decides whether to pass the request along).
All operational knobs are in a single config package reading from environment variables with documented defaults:
cfg := config.Load() // reads PORT, READ_TIMEOUT, MAX_TEXT_LENGTH, etc.Invalid environment values fall back to defaults silently. This follows the 12-factor app methodology and makes every configuration option discoverable.
The service uses only Go's standard library (net/http, encoding/json, regexp, log/slog). Go 1.22's method-based routing (mux.HandleFunc("POST /parse", ...)) removes the need for a third-party router. Zero external dependencies means zero supply-chain risk.
For structured clinical observations ("weight of 9st 3lbs"), regex is deterministic, requires no external services, adds zero latency, and is straightforward to test.
Extracted values are validated against plausible ranges (weight 20–500 kg, height 50–300 cm) to prevent false positives from incidental numbers (e.g. "walked 100m" is not a height).
Defence-in-depth at the HTTP boundary: Content-Type enforcement, DisallowUnknownFields, trailing data rejection (dec.More()), configurable text length limit, and middleware body size cap.
The server listens for SIGINT/SIGTERM and drains in-flight connections with a configurable timeout.
Two-stage build: golang:1.23-alpine → alpine:3.20. ~15 MB image, non-root user, HEALTHCHECK directive.
health-metric-api/
├── cmd/server/
│ └── main.go # Entry point — config, wiring, shutdown
├── internal/
│ ├── config/
│ │ ├── config.go # Centralised configuration from env vars
│ │ └── config_test.go # Defaults, overrides, invalid fallback
│ ├── handler/
│ │ ├── handler.go # HTTP handlers, Parser interface
│ │ └── handler_test.go # Handler integration tests
│ ├── parser/
│ │ ├── parser.go # MetricExtractor interface, Parser
│ │ ├── extractors.go # Weight and height extractors
│ │ ├── parser_test.go # Unit tests and benchmarks
│ │ └── example_test.go # Example tests (runnable documentation)
│ ├── model/
│ │ └── model.go # Request/response types
│ └── middleware/
│ ├── middleware.go # Logging, recovery, body size limit
│ └── middleware_test.go # Middleware unit tests
├── integration_test.go # End-to-end HTTP stack tests
├── .golangci.yml # Linter configuration
├── Dockerfile # Multi-stage production build
├── docker-compose.yml # One-command local startup
├── Makefile # Development task runner
└── go.mod