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 does not 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 are 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
41 })
42 } catch (err) {
43 res.status(500).json({ ok: false, error: err.message })
44 }
45})
Why this exists

Dynamic route offline coverage is not just cached data — it is 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: after this single bootstrap call,
2// dispatchToStore posts FLUX_WARM_CHUNKS to the service worker with
3// these paths. The SW fetches and caches each chunk immediately — so
4// when a user later navigates to /blog/some_slug they've never visited,
5// while fully offline, the SW can still serve the compiled route chunk
6// for the dynamic page template. Only the DATA comes from IDB (via
7// the LRU channel and idbGet, see ingestion/lru) — the CODE to render
8// it comes from this warmed chunk cache. Miss this step and an
9// unvisited dynamic route works offline for its data but 404s on the
10// JS needed to render it.
11
12// If you're not on Next.js, or have no dynamic routes backed by
13// Flux data, just return assets: [] unconditionally — this is the
14// 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 — worth keeping in mind when
2// 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 is 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

Any failure mode other than a 5xx simply falls back to serving existing IDB data — there is no scenario where a failed bootstrap call should clear or roll back what is already cached client-side.

Decoupled Microservice Aggregation

Because Flux accepts any JSON response adhering to { ok, data, assets }, a Node gateway can fan out across multiple internal microservices in parallel, aggregate the JSON payloads under the matching channel keys, and return the combined payload to the client in a single hop.

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.

Powered by Flux