Bootstrap Endpoints

Node (generic)

The framework-agnostic reference for a bootstrap endpoint — no assumption about your database or ORM. Covers the response contract itself, plus the one genuinely framework-specific piece: Next.js dynamic route chunk warming via assets[].

Prerequisites
  • Any Node HTTP layer — Express, Fastify, Koa, or bare http
  • For Postgres- or MongoDB-specific query patterns, see bootstrap/postgres or bootstrap/mongodb instead — this page stays data-layer-agnostic
  • Familiarity with api/bootstrap on the client side
1

The contract, independent of everything else

Every bootstrap endpoint, regardless of database or framework, answers to exactly this shape. Everything else on this page is implementation detail around producing it.
typescript
1// The bootstrap API contract — this is the whole spec, independent
2// of database, ORM, or framework:
3
4interface BootstrapResponse {
5 ok: boolean
6 data: Record<string, any> // keys match your client-side map
7 assets: string[] // Next.js dynamic route chunks — see Step 4
8}
9
10// GET (or POST, Flux doesn't care) request in, one JSON response out.
11// Everything else on this page is about HOW to build that response
12// on a plain Node backend with no assumption about your data layer.
2

A minimal route, data layer abstracted away

Swap getServices() / getTeam() for whatever actually fetches your data — raw SQL, an ORM, a call to another internal service. The bootstrap contract doesn't care.
server/routes/bootstrap.ts
1// server/routes/bootstrap.ts
2import express, { Router, Request, Response } from 'express'
3import { getServices, getTeam } from '../data/queries'
4
5const router: Router = express.Router()
6
7router.get('/public', async (req: Request, res: Response) => {
8 try {
9 const [services, team] = await Promise.all([
10 getServices(),
11 getTeam(),
12 ])
13
14 res.json({
15 ok: true,
16 data: { services, team },
17 assets: [],
18 })
19 } catch (err: any) {
20 res.status(500).json({ ok: false, error: err.message })
21 }
22})
23
24export default router
3

Works the same across Node frameworks

The differences between Express, Fastify, Koa, and a bare http server are entirely in request/response plumbing — none of it touches the contract itself.
typescript
1// This same contract works unmodified across any Node HTTP layer —
2// only the request/response plumbing changes:
3
4// Express — req.query, res.json({ ... }) (shown throughout this page)
5// Fastify — request.query, reply.send({ ... })
6// Koa — ctx.query, ctx.body = { ... }
7// Next.js API/route — see the assets[] extraction below, Step 4
8// is what actually differs for Next.js
9// Bare http/https — JSON.stringify() into res.end()
10
11// Nothing about dispatchToStore, idbSet, or the bootstrap module on
12// the client cares which of these produced the response — the
13// contract is the response shape, not the server framework.
4

assets[] — the one framework-specific piece

If you're on Next.js and have LRU channels backing dynamic routes, this is what makes an unvisited dynamic page work fully offline. Extracted server-side from .next/app-build-manifest.json, cached at module load.
javascript
1// server/routes/bootstrap.js — Next.js dynamic route chunk warming
2// This is the ONE thing that differs meaningfully by framework, and
3// it's opt-in: only relevant if you're on Next.js AND have LRU
4// channels backing dynamic routes (e.g. /blog/[slug]).
5const fs = require('fs')
6const path = require('path')
7
8// Cached at module load — .next/app-build-manifest.json doesn't change
9// while the process is running, so there's no reason to re-read it
10// on every request.
11let dynamicChunkPaths = null
12
13function getDynamicChunkPaths() {
14 if (dynamicChunkPaths) return dynamicChunkPaths
15 if (process.env.NODE_ENV === 'development') return []
16
17 const manifestPath = path.join(process.cwd(), '.next', 'app-build-manifest.json')
18 if (!fs.existsSync(manifestPath)) return []
19
20 const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
21 const pages = manifest.pages ?? {}
22
23 dynamicChunkPaths = Object.entries(pages)
24 // filter for routes containing a dynamic segment, e.g. /blog/[slug]
25 .filter(([route]) => route.includes('['))
26 .flatMap(([, chunks]) => chunks)
27 .filter((chunk) => chunk.startsWith('static/'))
28 .map((chunk) => `/_next/${chunk}`)
29
30 return dynamicChunkPaths
31}
32
33router.get('/public', async (req, res) => {
34 try {
35 const [services, team] = await Promise.all([getServices(), getTeam()])
36
37 res.json({
38 ok: true,
39 data: { services, team },
40 assets: getDynamicChunkPaths(), // empty [] in development, per Section 4.13
41 })
42 } catch (err) {
43 res.status(500).json({ ok: false, error: err.message })
44 }
45})
Why this exists

This is what the demo app and Section 17's final definition of Flux call out specifically: dynamic route offline coverage isn't just cached data — it's cached data and the compiled route chunk to render it with, both warmed by the same single bootstrap call. See the extended explanation below.

typescript
1// Why this exists at all (Section 4.13 / Section 17): after this
2// single bootstrap call, dispatchToStore posts FLUX_WARM_CHUNKS to
3// the service worker with these paths. The SW fetches and caches
4// each chunk immediately — so when a user later navigates to
5// /blog/some-slug they've never visited, while fully offline, the
6// SW can still serve the compiled route chunk for the dynamic page
7// template. Only the DATA comes from IDB (via the LRU channel and
8// idbGet, see ingestion/lru) — the CODE to render it comes from
9// this warmed chunk cache. Miss this step and an unvisited dynamic
10// route works offline for its data but 404s on the JS needed to
11// render it.
12
13// If you're not on Next.js, or have no dynamic routes backed by
14// Flux data, just return assets: [] unconditionally — this is the
15// only framework-specific piece of the entire bootstrap contract.
5

Extending to a 'user'-scope endpoint

The shape is identical — only the identity resolution and query filtering change, and that logic lives entirely inside your data-layer functions, not in the response contract.
javascript
1// A 'user'-scope endpoint follows the exact same shape — the only
2// addition is resolving the authenticated identity server-side and
3// filtering your data-layer calls by it. This is unchanged regardless
4// of what getDashboard()/getBilling() actually query underneath:
5
6router.get('/user', requireAuth, async (req, res) => {
7 const userId = req.user.id // from YOUR OWN auth middleware — never
8 // the client's BootstrapConfig.userId
9
10 try {
11 const [dashboard, billing] = await Promise.all([
12 getDashboard(userId),
13 getBilling(userId),
14 ])
15
16 res.json({
17 ok: true,
18 data: { dashboard, billing },
19 assets: [],
20 })
21 } catch (err) {
22 res.status(500).json({ ok: false, error: err.message })
23 }
24})
6

Status codes and how the client reacts

Worth knowing before you decide what to return on failure — the client's bootstrap module treats 5xx differently from every other failure mode.
typescript
1// How the client treats each response, from Section 4.13 — worth
2// keeping in mind when deciding what status code / body to return:
3
4// 2xx + { ok: true, data, assets } -> dispatched normally
5// 5xx -> cooldown reset to 0, so the
6// next online event retries
7// immediately, existing IDB
8// data keeps serving in the
9// meantime
10// any other failure (network error,
11// 4xx, malformed JSON) -> existing IDB data keeps
12// serving, NEVER rolled back
13
14// There's no special client behavior tied to a 4xx vs. a malformed
15// 200 — return a real 5xx for transient backend failures (so the
16// retry-immediately path kicks in) and reserve 4xx for genuine
17// client errors like a missing/invalid auth token on a 'user' scope
18// call.
Never roll back on a bootstrap failure

Per Section 4.13, any failure mode other than a 5xx simply falls back to serving existing IDB data — there's no scenario where a failed bootstrap call should clear or roll back what's already cached client-side.

For Postgres query patterns, see bootstrap/postgres. For MongoDB, see bootstrap/mongodb. For any non-Node backend language, see bootstrap/rest. For the client-side call this endpoint answers, see api/bootstrap.