Mock API Guide
Understand the in-memory data layer and how to swap it for real APIs.
Mock API Guide
SentinelGrid ships with a complete in-memory data layer so you can prototype every page without a backend. When you are ready, swap the imports for real API calls — the page components do not need to change.
Overview
The mock data layer lives in src/lib/data.ts. It exports typed arrays for every domain entity (incidents, services, deployments, alerts, etc.) plus a few helper functions for time-series generation.
What lives in data.ts
peopleteamsservicesincidentspostmortemsslosdeploymentsalertsalertRulesonCallShiftsescalationPolicieshostsk8sClusterserrorGroupsvulnerabilitiesauditEventsapiKeyswebhooksintegrationslogEntriestraceSpanschangeRequestsfeatureFlagsrunbooksuptimeRecordsregionsThe Data Layer
Each export is a typed array. Types are defined in src/lib/types.ts and imported into both data.ts and your page components. This means the TypeScript compiler catches shape mismatches at build time.
export interface Incident {
id: string;
title: string;
severity: "critical" | "high" | "medium" | "low" | "info";
status: "investigating" | "identified" | "monitoring" | "mitigated" | "resolved" | "postmortem";
serviceId: string;
startedAt: string;
resolvedAt?: string;
commanderId: string;
responderIds: string[];
summary: string;
}export const incidents: Incident[] = [
{
id: "INC-2024-0912-01",
title: "Payment gateway timeouts in EU region",
severity: "critical",
status: "investigating",
serviceId: "s6",
startedAt: "2024-09-12T08:14:00Z",
commanderId: "u2",
responderIds: ["u6", "u7"],
summary: "Elevated 5xx responses from payments-eu cluster.",
},
// ... 30+ more
];Helper functions resolve foreign-key references without joins:
export function person(id: string): Person {
return people.find((p) => p.id === id) ?? people[0];
}
export function team(id: string): Team | undefined {
return teams.find((t) => t.id === id);
}
export function dashboardStats(): DashboardStats { /* ... */ }Adding a New Entity
To add a new domain entity, follow these three steps:
- 1. Define the type in
src/lib/types.ts. - 2. Export a typed array from
src/lib/data.ts. - 3. Import and use it in your page component.
// 1. types.ts
export interface Runbook {
id: string;
title: string;
serviceId: string;
steps: string[];
lastUpdated: string;
}
// 2. data.ts
export const runbooks: Runbook[] = [
{ id: "rb1", title: "Restart payment worker", serviceId: "s6", steps: ["..."], lastUpdated: "2024-09-01" },
];
// 3. your page
import { runbooks } from "@/lib/data";
const myRunbooks = runbooks.filter((r) => r.serviceId === "s6");Swapping for a Real API
The pattern is to introduce a thin api/ module that mirrors the shape of the mock exports. Pages import from api/ instead of data.ts, so the swap is a one-line change per file.
// Real implementation (server component or route handler)
import type { Incident } from "@/lib/types";
export async function getIncidents(): Promise<Incident[]> {
const res = await fetch(`${process.env.API_URL}/incidents`, {
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
next: { revalidate: 30 },
});
if (!res.ok) throw new Error("Failed to load incidents");
return res.json();
}Next.js fetch
fetch with caching options. Use next: { revalidate: 30 } for ISR-style caching, or cache: "no-store" for always-fresh data.For server components, fetch directly. For client components, create a Route Handler at app/api/incidents/route.ts and call it with fetch("/api/incidents") from the client.
Zustand Stores
Client-side UI preferences live in src/lib/store.ts as Zustand stores. The store is hydrated from localStorage on mount via the UIStoreHydration component in the root layout.
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface UIState {
sidebarCollapsed: boolean;
toggleSidebar: () => void;
recentPages: { href: string; title: string; visitedAt: number }[];
pushRecent: (page: { href: string; title: string; visitedAt: number }) => void;
savedFilters: Record<string, unknown>;
}
export const useUIStore = create<UIState>()(
persist(
(set) => ({
sidebarCollapsed: false,
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
recentPages: [],
pushRecent: (page) =>
set((s) => ({
recentPages: [page, ...s.recentPages.filter((p) => p.href !== page.href)].slice(0, 10),
})),
savedFilters: {},
}),
{ name: "sentinelgrid-ui" }
)
);Why Zustand?
persist middleware syncs to localStorage automatically.