All posts

Designing an Analytics REST API

July 27, 20267 min read

One envelope everywhere

Pick a response shape and never deviate:

{ "success": true,  "data": { } }
{ "success": false, "error": "Site not found" }

The value is not elegance, it is that every client writes one handler. Endpoints that return a bare array on success and an object on failure force per-endpoint branching in every consumer.

Date ranges: named and explicit

Accept both:

?dateRange=30d
?dateRange=custom:2026-01-01:2026-03-31

Named ranges cover most calls and are readable in logs and cache keys. The custom form covers the rest. Parse once, in one place, and return a { start, end } — date arithmetic scattered across route handlers is where off-by-one-day bugs breed.

Reject unparseable ranges with a 400 rather than silently defaulting. A typo that quietly returns 30 days of data is worse than an error.

Cache headers should match reality

If an endpoint is cached server-side for sixty seconds, say so:

Cache-Control: public, max-age=60

Real-time endpoints get ten seconds. Historical aggregates that can no longer change get much longer. The common mistake is uniform no-cache, which throws away the work the server-side cache just did.

Errors that help without leaking

A developer needs enough to fix the call. An attacker must not learn your internals.

res.status(status).json({ success: false, error: safeMsg(error, status) });

Return the real message for 4xx — those are the caller's fault and they need it. Return a generic message for 5xx, and log the detail server-side. A stack trace in a 500 response is an information disclosure.

Validate scope before work

Check site access before running the query, not after:

router.use('/:siteId', validateSiteId, authorizeSiteAccess);

Doing it afterwards means an unauthorised request still costs a full table scan, which is a denial-of-service vector as well as a leak of timing information.

Version when you break

Additive changes need no version. Removing a field or changing a type does.

/api/v2/analytics/... is unglamorous and it works. The alternative — changing a response shape in place — breaks every integration silently, and you find out through support tickets.

Document with OpenAPI, and test the drift

Generate an OpenAPI spec and keep it in lock-step with the implementation. A test asserting that the spec's operation ids match the actual route set catches drift the moment it appears, rather than months later when someone trusts the docs.

Try InsightsTrack free

Open-source, self-hosted, privacy-first analytics — explore the live demo, no install required.

Open live dashboard