Back to projects
Infrastructure Experimental 2023

Prometheus

Observability prototype: a React dashboard wired to an Express service. The server exports Prometheus metrics, and AJV validates every response payload at runtime.

Metrics + schema validation, full-stack

  • TypeScript
  • React
  • Node.js
  • Express
  • Prometheus
  • AJV

Why I built this

A lot of full-stack demos skip over the contract between client and server. The client trusts whatever the API sends back, the API trusts what it does, and when something breaks in production you usually find out hours later through Sentry.

I wanted to build a small thing that took the contract seriously. Every response gets validated against a schema before it leaves the server, and the server emits Prometheus metrics so the dashboard can show its own state next to whatever else it’s fetching.

What it does

  • React + Tailwind + MUI dashboard fetches the current server time (in epoch seconds) from an authenticated endpoint.
  • Express server validates every response against an AJV schema before sending. The validation is on the server boundary, not in the client.
  • /metrics exposes Prometheus counters and histograms for request count, error rate, and latency.
  • Tests cover both schema validation and the metrics output.
  • Live demo: peppy-puffpuff-62981e.netlify.app.

Architecture

Two things in this diagram are worth knowing. AJV runs inside the response path, not just in tests, so if a response shape drifts the server refuses to send it. And the metrics come from the same process that serves the API. There’s no agent or sidecar. prom-client instruments the handlers in process and Prometheus scrapes the endpoint.

Technical decisions

AJV instead of writing a validator by hand. You can reuse the schemas on the client too if you want to validate incoming data the same way. Compiled validators are also faster than chains of if-statements.

Prometheus format on the server itself. Pushing metrics to a SaaS works but you lose the option to keep alerts close to the deploy. The exposition format is a one-line dependency.

Tailwind plus MUI. Tailwind for layout, MUI for ready-made widgets. Picking one of them would have meant rebuilding the other.

No state framework on the client. No Redux, no React Query. It’s a dashboard. A fetch hook is enough.

import Ajv from "ajv";

const ajv = new Ajv({ removeAdditional: "all" });

const TimeResponse = ajv.compile({
  type: "object",
  properties: {
    epochSeconds: { type: "integer", minimum: 0 },
  },
  required: ["epochSeconds"],
  additionalProperties: false,
});

app.get("/api/time", (_req, res) => {
  const body = { epochSeconds: Math.floor(Date.now() / 1000) };
  if (!TimeResponse(body)) {
    return res.status(500).json({ error: "schema violation" });
  }
  res.json(body);
});

Tests as a contract

Every endpoint has a paired schema test. The runtime validation catches drift in production. The tests catch it before it ships.

Prometheus repository showing the passing test suite output.
The test suite covers schema-validation behaviour and the metrics exposition format.

What I learned

The first time someone refactored a response field, the server refused to send it and the line number was right there in the logs. That was much faster than chasing an undefined in the browser.

The Prometheus format is simpler than I thought. A few Counter and Histogram instances and you have a useful dashboard.

Full-stack projects feel real once at least one thing crosses the whole stack. Observability is a good fit because it makes you think about the contract.

Next steps

  • Hook the Prometheus endpoint up to a hosted Grafana so the live demo actually shows metrics, not just a clock.
  • Add a second endpoint with more complex payloads so you can see schema validation catching real drift.
  • Try rebuilding the server with Hono on Bun and compare the latency histogram.
  • Pull the chart components, metric tiles, and the schema-aware fetch hook into a small reusable kit.

Source: github.com/uzairali19/prometheus.