Mock API Guide

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

people
teams
services
incidents
postmortems
slos
deployments
alerts
alertRules
onCallShifts
escalationPolicies
hosts
k8sClusters
errorGroups
vulnerabilities
auditEvents
apiKeys
webhooks
integrations
logEntries
traceSpans
changeRequests
featureFlags
runbooks
uptimeRecords
regions

The 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.

src/lib/types.ts (excerpt)
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;
}
src/lib/data.ts (excerpt)
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:

Helpers
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.
Adding a Runbook entity
// 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.

src/lib/api/incidents.ts
// 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

Next.js 16 extends the native 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.

src/lib/store.ts (excerpt)
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?

Zustand keeps client state outside React's tree, so it survives route transitions without prop drilling. The persist middleware syncs to localStorage automatically.

Command Palette

Search for a command to run...