-
Notifications
You must be signed in to change notification settings - Fork 7
Request lifecycle
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.
When a user interacts with StartER, each action follows a precise sequence: from the browser request to the response returned by the server.
- The browser sends a single HTTP request to the server (GET, POST, PUT, DELETE, etc.)
- The Express server parses and processes this request through a series of middleware
- The content is generated according to the corresponding path: JSON data for an API or a server-rendered HTML page
- The server sends an HTTP response to the browser (containing the JSON-formatted data or the rendered HTML page)
- 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.
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...
});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.
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);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 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.)
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.
The process takes place in 4 steps in entry-server.tsx:
-
Get a routing context
const context = await query( new Request(`${req.protocol}://${req.get("host")}${req.originalUrl}`), );
-
Create a static router for the SSR
const router = createStaticRouter(dataRoutes, context);
-
Render with a
StaticRouterProviderconst { pipe } = renderToPipeableStream( <StrictMode> <StaticRouterProvider router={router} context={context} /> </StrictMode>, );
-
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.
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.
- 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/awaitneeded for queries). Use asynchronous operations only for genuinely async resources (e.g., calls to external APIs or sending emails).
AI co-creation
Getting started
Explanations
How-To Guides
Reference
Digging deeper