August 17, 2026
How I Built a Real-Time Analytics Dashboard with TypeScript And Why It Almost Broke Me
A late-night build story about charts, WebSockets, bad assumptions, and the TypeScript patterns that finally made my dashboard feel…

By Shayan Khan
4 min read
A late-night build story about charts, WebSockets, bad assumptions, and the TypeScript patterns that finally made my dashboard feel "alive."
It started with a Slack message I still hate remembering.
"Can we see live numbers… like, right now?"
Not tomorrow. Not after the standup. Right now.
I was a frontend developer with a half-working admin panel, a messy JavaScript codebase, and zero confidence that anything in production was "real-time."
So I did what most of us do when panic meets pride:
I promised a dashboard.
This is the story of how I built it with TypeScript, almost shipped a disaster, and learned more about architecture in three weeks than I had in the previous year.
The Night I Realized JavaScript Was Lying to Me
The old dashboard was classic "works on my machine" energy.
- Numbers arrived as
any - Chart libraries got random shapes
- One API returned
user, another returnedusers_count - The UI sometimes showed
NaNlike it was proud of it
I remember hovering over a variable in VS Code and seeing… nothing useful.
No types. No contract. Just vibes.
That night I made a rule:
If the data shape is not typed, it does not enter the UI.
TypeScript wasn't a "nice-to-have" anymore.
It was the only way I could sleep.
What I Actually Decided to Build
I scoped it hard.
Viral dashboards look fancy. Useful dashboards are boring and clear.
MVP Features
- Live visitor count
- Conversion rate for the last 24 hours
- Top pages
- Error rate
- A "last updated" heartbeat so people trusted the data
Stack I Chose
- Next.js — App Router
- TypeScript — strict mode, no excuses
- Recharts — charts
- WebSockets — live updates
- Zod — validate every payload before it touched React
I didn't need a perfect product.
I needed a product my team would stop arguing about.
The TypeScript Move That Changed Everything
Instead of "fetching JSON and hoping," I wrote the dashboard as a contract first.
type DashboardMetric = {
id: string;
label: string;
value: number;
deltaPercent: number;
updatedAt: string; // ISO
};
type LiveDashboardPayload = {
visitorsNow: number;
conversionRate: number;
topPages: Array<{ path: string; views: number }>;
errorRate: number;
metrics: DashboardMetric[];
};
type DashboardMetric = {
id: string;
label: string;
value: number;
deltaPercent: number;
updatedAt: string; // ISO
};
type LiveDashboardPayload = {
visitorsNow: number;
conversionRate: number;
topPages: Array<{ path: string; views: number }>;
errorRate: number;
metrics: DashboardMetric[];
};Then I wrapped the WebSocket messages with Zod:
import { z } from "zod";
const LiveDashboardSchema = z.object({
visitorsNow: z.number().nonnegative(),
conversionRate: z.number().min(0).max(1),
topPages: z.array(
z.object({
path: z.string(),
views: z.number().int().nonnegative(),
})
),
errorRate: z.number().min(0).max(1),
metrics: z.array(
z.object({
id: z.string(),
label: z.string(),
value: z.number(),
deltaPercent: z.number(),
updatedAt: z.string().datetime(),
})
),
});import { z } from "zod";
const LiveDashboardSchema = z.object({
visitorsNow: z.number().nonnegative(),
conversionRate: z.number().min(0).max(1),
topPages: z.array(
z.object({
path: z.string(),
views: z.number().int().nonnegative(),
})
),
errorRate: z.number().min(0).max(1),
metrics: z.array(
z.object({
id: z.string(),
label: z.string(),
value: z.number(),
deltaPercent: z.number(),
updatedAt: z.string().datetime(),
})
),
});The first time a bad payload arrived, the UI didn't crash.
It ignored it.
That felt like magic — the quiet kind.
The Part Nobody Puts on LinkedIn: WebSocket Hell
Real-time sounds cool until your socket reconnects 40 times and your chart redraws like it's having a panic attack.
My first version:
- Reconnected forever
- Duplicated listeners
- Stacked chart updates on top of old ones
- Made the browser cry
So I built a tiny state machine in my head:
idle → connecting → live → reconnecting → live
And I forced one rule in code:
let socket: WebSocket | null = null;
function connect() {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
return;
}
socket = new WebSocket(process.env.NEXT_PUBLIC_WS_URL!);
socket.onmessage = (event) => {
const parsed = LiveDashboardSchema.safeParse(JSON.parse(event.data));
if (!parsed.success) {
console.warn("Invalid live payload", parsed.error);
return;
}
setDashboard(parsed.data);
};
}let socket: WebSocket | null = null;
function connect() {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
return;
}
socket = new WebSocket(process.env.NEXT_PUBLIC_WS_URL!);
socket.onmessage = (event) => {
const parsed = LiveDashboardSchema.safeParse(JSON.parse(event.data));
if (!parsed.success) {
console.warn("Invalid live payload", parsed.error);
return;
}
setDashboard(parsed.data);
};
}Suddenly the dashboard stopped feeling haunted.
Designing for Humans, Not for Screenshots
I almost overdesigned it.
Big gradients , Fancy cards , Too many colors , A "cyberpunk ops center" vibe.
Then my PM asked one question:
"Where do I look first?"
So I deleted half the UI.
Final layout:
- Hero metric row (4 numbers max)
- One main chart
- One table
- One status line:
Live • updated 2s ago
TypeScript helped here too. When every metric had a type, the UI couldn't invent random widgets. Constraints made the design honest.
What shipping taught me (the real SEO juice is honesty)
Developer celebrating a successful deploy
When we launched, people didn't praise my "architecture."
They said:
- "I finally trust these numbers."
- "It doesn't freeze anymore."
- "Wait… this is actually live?"
That's when it clicked.
A dashboard is not a chart gallery. It's a trust machine.
Lessons I'm keeping forever
- Type the boundary. Validate every external payload.
- Strict mode is kindness. Future-you needs it.
- Real-time needs a state machine, even a small one.
- Delete UI until the story is obvious.
- Ship ugly-but-true before pretty-but-fake.
If you're building your first TypeScript dashboard
Start smaller than your ego wants.
Tonight, do only this:
- Define one payload type
- Validate it with Zod
- Render three metrics
- Add a "last updated" timestamp
- Then — and only then — add live sockets
Don't begin with "Netflix for analytics." Begin with "numbers I can defend in a meeting."
That's how I finally built a dashboard that didn't just look smart.
It made me look reliable.
Soft CTA
If you're stuck on typing live data or WebSocket reconnects, drop a comment with your stack. I'll reply with a pattern that fits your setup.
Clap if this saved you a late night. Follow for more build stories on TypeScript, AI, and shipping real products without the fake guru energy.