Data Flow & State¶
This page traces a request from a button click to a backend microservice and back, and explains how the platform divides responsibility between server state (React Query) and client state (Zustand).
The Request Path¶
Every backend interaction flows through the same layers. The browser speaks REST/JSON to a Next.js route handler; the route handler speaks gRPC to the backend.
sequenceDiagram
participant C as Component
participant H as apiHook (React Query)
participant S as service/*
participant AX as Axios (apis/index.ts)
participant R as app/api/* route
participant CB as Cerbos
participant G as utils/grpc.ts
participant MS as Backend microservice
C->>H: call hook (useQuery / useMutation)
H->>S: invoke service fn
S->>AX: axios.get/post(...)
AX->>AX: attach JWT from cookies
AX->>R: REST/JSON request
R->>CB: authorize(principal, resource, action)
CB-->>R: allow / deny
R->>G: typed gRPC client call
G->>MS: Protocol Buffers over gRPC
MS-->>G: proto response
G-->>R: { success, data, status }
R-->>AX: JSON
AX-->>H: data (or 401 → refresh flow)
H-->>C: data / isLoading / error
The type → service → hook → component pattern¶
A single feature is wired through four small, predictable files:
proto/projects.proto # contract (compiled to proto-types/projects.ts)
types/createProject.types.ts # TS request/response types (re-exported from proto)
services/createProject.services.ts# axios call to app/api/projects
hooks/apiHooks/useCreateProject # useMutation wrapper + cache invalidation
components/pages/projects/... # form that calls the hook
A representative end-to-end trace — "Create Project":
- A form (React Hook Form + Ant Design) submits to
useCreateProject(). - The hook runs a React Query
useMutationwhosemutationFncalls thecreateProjectservice. - The service issues
axios.post('projects', payload); the Axios interceptor attaches the JWT. - The request hits
app/api/projects/route.ts, which authorizes via Cerbos, then calls theProjectsServicegRPC client fromutils/grpc.ts. - The backend returns a typed
CreateProjectResponse; the route handler returns JSON. - On success the hook invalidates
['projects']so the list re-fetches, and navigates to/projects/[id].
Backend Transports¶
The platform uses three transports depending on the interaction:
flowchart LR
subgraph Client
B[Browser]
end
subgraph NextServer["Next.js server"]
API[app/api/*]
end
subgraph BE[Backend]
G[gRPC microservices]
WS[Socket.io server]
end
B -->|"REST / JSON (Axios)"| API
API -->|"gRPC + protobuf (nice-grpc)"| G
B <-->|"WebSocket (Socket.io)"| WS
| Transport | Used for | Where configured |
|---|---|---|
| REST/JSON (Axios) | All standard browser→server requests | apis/index.ts |
| gRPC (nice-grpc) | Server→backend microservice calls | utils/grpc.ts, utils/microservices.ts |
| WebSocket (Socket.io) | Realtime crawler-log streaming, activity feeds | hooks/useLogStream.hook.tsx, store/crawlerLogsStore.ts |
The gRPC client factory resolves a cluster address of the form
{service}.{env}.svc.grepsr.net:443, applies TLS based on CONFIG_SECURE_CONNECTION, and raises
the max message size to 30 MB (large report payloads exceed the 4 MB default). gRPC status codes
are mapped to HTTP status codes (utils/mapGrpcToHttpStatus) so the browser sees conventional HTTP
semantics.
State Management¶
The platform draws a hard line between server state and client state.
flowchart TB
subgraph RQ["React Query — SERVER state"]
Q["Queries: projects, reports, activities, ..."]
M["Mutations: createProject, bulkExport, deleteRule, ..."]
end
subgraph ZU["Zustand — CLIENT state (store/)"]
AppS["app: projects cache, selectedReport, filters, schedule params"]
AuthS["auth: userData, tokens, impersonateUser"]
PageS["pageRef: element refs"]
LogS["crawlerLogs: live log stream"]
end
Backend[(Backend)] --> RQ
RQ -. hydrates .-> ZU
UI[Components] --> RQ
UI --> ZU
React Query (server state)¶
- Configured in
providers/QueryProvider.tsx. Anything originating from the backend — lists, details, activity feeds — is a query; anything that writes is a mutation. - Defaults favour explicit control: a short
staleTime,refetchOnWindowFocusdisabled, and a single retry. Freshness is driven by targetedinvalidateQueriesafter mutations rather than aggressive background refetching. - Optimistic updates (
utils/optimisticUpdate) apply changes to the cache immediately and roll back on error for latency-sensitive interactions.
Zustand (client state)¶
- Configured in
store/index.ts, combining slices with the Redux DevTools middleware. Accessed via theuseStore()hook.
| Slice | Holds |
|---|---|
app |
Cached project list, the currently selected report, filtered report subsets, schedule-form parameters. |
auth |
userData, JWT tokens, and the impersonateUser flag for admin impersonation. |
pageRef |
DOM/element references shared across the page. |
crawlerLogs |
The live log stream populated by the Socket.io hook. |
Rule of thumb
If the data came from (or is going to) the backend, it belongs in React Query. If it is
ephemeral UI or session context that the client owns, it belongs in Zustand. Avoid copying
server data into Zustand except as a deliberate cache (as app.projects does for cross-route
convenience).
Error Handling¶
Errors are handled at consistent layers:
- Service layer — services surface a typed error from the Axios call.
- Axios interceptor (
apis/index.ts) — a401triggers the token-refresh-and-retry flow (see Authentication & Authorization); other errors propagate. - React Query —
isError/errorare exposed to components; mutationonErrorhandlers run rollbacks where applicable. - UI — failures render as toasts/notifications via the Toaster providers.