Skip to content

Request lifecycle

rocambille edited this page Jun 4, 2026 · 2 revisions

Summary: The request-response cycle is at the heart of any web application. In StartER, it is supported by a unified architecture combining Express (API) and React (interface), all managed by one server.

Overview

When a user interacts with StartER, each action follows a precise sequence: from the browser request to the response returned by the server.

  1. The browser sends a single HTTP request to the server (GET, POST, PUT, DELETE, etc.)
  2. The Express server parses and processes this request through a series of middleware
  3. The content is generated according to the corresponding path: JSON data for an API or a server-rendered HTML page
  4. The server sends an HTTP response to the browser (containing the JSON-formatted data or the rendered HTML page)
  5. The React client hydrates (for web pages) and becomes interactive
┌──────────────┐     HTTP Request (1) ┌─────────────┐    Data Request     ┌─────────────┐
│              │───────────────────>  │             │───────────────────> │             │
│   Browser    │                      │   Express   │                     │  Database   │
│              │  <───────────────────│  Server (2) │  <──────────────────│             │
└──────────────┘    HTTP Response (4) └─────────────┘       Data (3a)     └─────────────┘
        ▲                                  ▲  │
        │                                  │  │
        │                        HTML (3b) │  ▼ Page Request
┌──────────────┐                      ┌─────────────┐
│              │                      │             │
│ React Client │  <────Hydration───── │  React SSR  │
│  (Browser)   │          (5)         │  (Server)   │
└──────────────┘                      └─────────────┘

This diagram illustrates the flow of data between the browser, the Express server, the database, and the React SSR engine.

The cycle may vary depending on the type of request (API or page) and the user's authentication status.

Implementation in StartER

Server architecture

The server.ts file is the entry point for the application. It configures a single Express server, responsible for:

  • serving API routes
  • rendering React pages via Server-Side Rendering (SSR).
const app = express();

// Express API routes
app.use((await import("./src/express/routes")).default);

// Middleware "catch-all" for SSR
app.use(/(.*)/, async (req, res, next) => {
  // SSR logic...
});

API request processing

Requests to the API (prefixed with /api) are handled by Express using a modular routing system. Each module is isolated in src/express/modules, making the architecture easy to extend.

Routing

Routes are organized by modules in the src/express/modules folder to be used in src/express/routes.ts. This could be:

/* ************************************************************************ */

import authRoutes from "./modules/auth/authRoutes";

router.use(authRoutes);

/* ************************************************************************ */

import itemRoutes from "./modules/item/itemRoutes";

router.use(itemRoutes);

/* ************************************************************************ */

import userRoutes from "./modules/user/userRoutes";

router.use(userRoutes);

/* ************************************************************************ */

Note

The src/express/routes.ts file provides the importAndUse function which combines dynamic import() and router.use(). This keeps the routes file lightweight and makes it easy to add or remove modules.

// Utility to dynamically import a module and register it on the router
const importAndUse = async (path: string) =>
  router.use((await import(path)).default);

await importAndUse("./modules/auth/authRoutes");
await importAndUse("./modules/item/itemRoutes");
await importAndUse("./modules/user/userRoutes");

Each module defines its own routes. For example, for the src/express/modules/item module in itemRoutes.ts:

const BASE_PATH = "/api/items";
const ITEM_PATH = "/api/items/:itemId";

router.get(BASE_PATH, itemActions.browse);
router.get(ITEM_PATH, itemActions.read);

router.post(BASE_PATH, itemValidator.validate, itemActions.add);

router.route(ITEM_PATH)
  .all(checkAccess)
  .put(itemValidator.validate, itemActions.edit)
  .delete(itemActions.destroy);

Intermediate middleware

Before reaching their final action, requests pass through a chain of middleware:

  • Rate limiting by IP address
  • JSON parsing (express.json())
  • Authentication verification via JWT
  • Data validation with Zod
  • ...

Actions and responses

Actions are responsible for final processing and response generation:

const add: RequestHandler = (req, res) => {
  const insertId = itemRepository.create(req.body);

  res.status(201).json({ insertId });
};

const read: RequestHandler = (req, res) => {
  res.json(req.item);
};

Responses to an API request can be:

  • JSON objects (for GET and POST requests)
  • HTTP status codes (for PUT and DELETE requests)
  • Error codes (400, 401, 403, 404, etc.)

Page request processing

For all other requests (those not targeting /api), StartER uses React in Server-Side Rendering (SSR) mode to generate the initial HTML of the pages.

SSR process

The process takes place in 4 steps in entry-server.tsx:

  1. Get a routing context

    const context = await query(
      new Request(`${req.protocol}://${req.get("host")}${req.originalUrl}`),
    );
  2. Create a static router for the SSR

    const router = createStaticRouter(dataRoutes, context);
  3. Render with a StaticRouterProvider

    const { pipe } = renderToPipeableStream(
      <StrictMode>
        <StaticRouterProvider router={router} context={context} />
      </StrictMode>,
    );
  4. Send the HTML response to the client

    res.status(200).set("Content-Type", "text/html; charset=utf-8");
    res.write(htmlStart);
    // ...
    pipe(transformStream);

These four steps result in a complete HTML page sent to the browser, ready to be "rehydrated" on the client side.

Note

See the full code for details.

Client-side hydration

Once the initial HTML is loaded, the client JavaScript takes over: it reactivates the React components already present in the page and makes the interface interactive.

hydrateRoot(
  root,
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>,
);

This hydration uses the data preloaded during the SSR to avoid repeating the initial queries.

Note

See the full code for details.

Best practices and use cases

  • Keep actions lightweight: delegate business logic to services or repositories.
  • Understand the I/O model: StartER uses SQLite's synchronous API for database calls (no async/await needed for queries). Use asynchronous operations only for genuinely async resources (e.g., calls to external APIs or sending emails).

See also

Clone this wiki locally