Express en US - rocambille/start-express-react GitHub Wiki
Express is a minimalist and flexible web framework, ideal for quickly building HTTP servers or APIs. Here is a small example to check if you have mastered the basics of the framework:
import express from "express";
const app = express();
const port = 5173;
app.get("/api", (req, res) => {
res.send("hello, world!");
});
app.listen(port, () => {
console.info(`Listening on http://localhost:${port}`);
});If you see the message "hello, world!" returned in an HTTP response, you have mastered the basics of the framework.
Otherwise, we recommend reviewing the Express documentation before proceeding further. In particular, make sure you're comfortable with routing in Express.
In StartER, the creation of the "one server" relies on the routes defined in src/express/routes.ts.
const app = express();
// ...
app.use((await import("./src/express/routes")).default);
// ...The src/express/routes.ts file declares and exports an instance of express.Router.
import { Router } from "express";
const router = Router();
// ...
export default router;You can define routes with this router mini-application, just like with the main application.
A "simple" Express route combines an HTTP method (get, post, etc.), a URL path, and a callback function.
router.get("/api", (req, res) => { /* ... */ });
/* β β β
method path callback
*/The (req, res) => { /* ... */ } callback is a function that accesses the request object (req) and the response object (res) in the request lifecycle of the application. This function can:
- Execute code,
- Make changes to the request and response objects,
- End the request lifecycle.
For example, to end the cycle by sending a "hello, world!" response:
import { Router } from "express";
const router = Router();
/* ************************************************************************ */
router.get("/api", (req, res) => {
res.send("hello, world!");
});
/* ************************************************************************ */
export default router;| Method | Path | Callback |
router. get ("/api",(req, res) => {
res.send("hello, world!");
});
|
router.get( "/api" ,(req, res) => {
res.send("hello, world!");
});
|
router.get("/api", (req, res) => {
res.send("hello, world!");
} );
|
In this example, the callback is inserted directly into the route declaration.
Alternatively, you can separate the callback declaration from the route declaration.
import { type RequestHandler, Router } from "express";
const router = Router();
/* ************************************************************************ */
// Callback declaration
const sendHelloWorld: RequestHandler = (req, res) => {
res.send("hello, world!");
};
// Route declaration
router.get("/api", sendHelloWorld);
/* ************************************************************************ */
export default router;Separating the declarations allows you to move the callback code into separate files, and keep only the route declarations in the routes.ts file: one file = one responsibility.
import { type RequestHandler, Router } from "express";
const router = Router();
/* ************************************************************************ */
// Callback import
import { sendHelloWorld } from "./path/to/module";
// Route declaration
router.get("/api", sendHelloWorld);
/* ************************************************************************ */
export default router;This practice is the foundation of building modules in the Express section of StartER.
Express is a minimalist framework that doesn't impose any particular file organization. In StartER, we've chosen to frame the Express section with a modular architecture.
Modular architecture consists of grouping functionalities by theme, each module being autonomous and dedicated to a specific resource.
Most often, a theme corresponds to a resource served by the web API. For example, let's recreate a module for an item resource (in this example, an item is a dummy object with a title: the item module is provided by default in the StartER code). The first step is to create an item folder in the src/express/modules folder. The second step is to create a routes "sub-file" in our item folder:
src/express
βββ routes.ts
βββ modules
βββ item
βββ itemRoutes.ts
The itemRoutes.ts file is the entry point for the item module: it must declare and export an isolated router for the "items" routes.
import { Router } from "express";
const itemRoutes = Router();
// ...
export default itemRoutes;You can then use this item module in src/express/routes.ts:
// ...
import itemRoutes from "./modules/item/itemRoutes";
router.use(itemRoutes);
// ...Tip
The file src/express/routes.ts provides a utility function importAndUse to import and use routes in one call.
const importAndUse = async (path: string) =>
router.use((await import(path)).default);
// ...
await importAndUse("./modules/item/itemRoutes");At this point, the item module will build itself "freely" by adding whatever is needed to the item routes. After a few files, this could result in an item folder like this:
src/express
βββ routes.ts
βββ modules
βββ item
βββ itemActions.ts
βββ itemParamConverter.ts
βββ itemRepository.ts
βββ itemRoutes.ts
βββ itemValidator.ts
The "free" approach depends on your own practices and habits, and one module doesn't have to look like another. Some habits that may justify adding a file to a module (non-exhaustive list):
- One file = one responsibility. This is the basic principle that will guide your choices.
- Isolate reusable functionality between multiple modules.
- Repeat a pattern for your modules.
Do modules until you find an organization that resonates with you.
Trust your logic... and be inspired by others'.
π‘ To create a new Express module from the item module, use the clone script:
npm run make:clone src/express/modules/item src/express/modules/post Item PostThis script duplicates all module files and automatically replaces the Item identifiers with Post. This gives you a complete Express module ready to customize.
Once the routes are in place, it's time to isolate the business logic into reusable actions.
To share our logic with you, when we built our item module, we created a first file alongside itemRoutes.ts: an itemActions.ts file to declare the callbacks used by the item routes.
const browse = (req, res) => {
const items = /* we will tackle this next */;
res.json(items);
};
export default { browse };The browse action is an importable callback that can be used in itemRoutes.ts:
import { Router } from "express";
const itemRoutes = Router();
/* ************************************************************************ */
import itemActions from "./modules/item/itemActions";
itemRoutes.get("/api/items", itemActions.browse);
/* ************************************************************************ */
export default itemRoutes;This approach makes your code more readable and makes it easier to test actions independently of routes.
A TypeScript word: To enable type inference on req and res objects, we recommend typing your action with Express RequestHandler type.
import type { RequestHandler } from "express";
const browse: RequestHandler = (req, res) => {
const items = /* we will tackle this next */;
res.json(items);
};
export default { browse };To continue building the item module, we need to complete the browse action seen previously with "item" data.
const browse = (req, res) => {
const items = /* time to get data */;
res.json(items);
};A first "basic" approach would be to write the data directly into browse.
const browse = (req, res) => {
const items = [
{ id: 1, title: "Stuff" },
{ id: 2, title: "Doodads" },
];
res.json(items);
};This is an acceptable step for a prototype, but not ideal in the long term.
In our logic, data access must be isolated from actions: one file = one responsibility.
This is why we created an itemRepository.ts file in the src/express/modules/item folder to contain the data access layer for our items. The word "repository" is commonly used in this context. According to Symfony documentation:
When you query for a particular type of object, you always use what's known as its "repository". You can think of a repository as a [...] class whose only job is to help you fetch entities of a certain class..
To vary the writing styles, we took this excerpt literally and built a class in the itemRepository.ts file.
class ItemRepository {
// ...
}
export default new ItemRepository();In this class, we will provide methods to retrieve and modify item data. A first method to retrieve all items:
class ItemRepository {
readAll() {
return [
{ id: 1, title: "Stuff" },
{ id: 2, title: "Doodads" },
];
}
}
export default new ItemRepository();We can now use our repository in the item module:
import itemRepository from "./itemRepository";
const browse = (req, res) => {
const items = itemRepository.readAll();
res.json(items);
};
export default { browse };All item actions and the complete repository (linked to the database) are available in the following files:
See also:
StartER uses Express param converters, a convenient mechanism for transforming URL identifiers into business objects. This approach has two advantages:
- Invalid IDs are detected immediately.
- No need to load the entity in each route.
The itemParamConverter.ts file shows how to implement this functionality:
import type { RequestParamHandler } from "express";
import itemRepository from "./itemRepository";
const convert: RequestParamHandler = (req, res, next, itemId) => {
const item = itemRepository.read(+itemId);
if (item == null) {
res.sendStatus(404);
} else {
req.item = item; // Store item in req to use it next
next();
}
};
export default { convert };A TypeScript word: The base Request type in Express does not define typing for custom properties like req.item. To complete typing with your properties, we recommend extending Express Request type using declaration merging (see the manual).
import type { RequestParamHandler } from "express";
import itemRepository from "./itemRepository";
/* ************************************************************************ */
declare global {
namespace Express {
interface Request {
item: Item; // Extends Request interface of Express
}
}
}
/* ************************************************************************ */
const convert: RequestParamHandler = (req, res, next, itemId) => {
// ...
};
export default { convert };To enable the converter, simply associate it with the itemId parameter in itemRoutes.ts:
import itemParamConverter from "./itemParamConverter";
itemRoutes.param("itemId", itemParamConverter.convert);Then, any route containing :itemId in its path will automatically trigger the converter. For example:
import itemParamConverter from "./itemParamConverter";
itemRoutes.param("itemId", itemParamConverter.convert);
/* ************************************************************************ */
itemRoutes.get("/api/items/:itemId", itemActions.read);
/* ************************************************************************ */The read action in itemActions.ts can then directly access the item object without worrying about retrieval or validation:
const read: RequestHandler = (req, res) => {
res.json(req.item); // item is fetched and valid
};Before saving or modifying data, it is essential to verify its compliance. StartER relies on Zod to perform this validation on server side.
The itemValidator.ts file illustrates our approach:
import type { RequestHandler } from "express";
import { type ZodError, z } from "zod";
/* ************************************************************************ */
// Validation schema
const itemDTOSchema = z.object({
id: z.number().optional(),
title: z.string().max(255),
user_id: z.number(),
});
/* ************************************************************************ */
// Validation middleware
const validate: RequestHandler = (req, res, next) => {
try {
// Valider et transformer les donnΓ©es d'entrΓ©e
req.body = itemDTOSchema.parse({
...req.body,
user_id: Number(req.auth.sub),
});
next();
} catch (err) {
const { issues } = err as ZodError;
res.status(400).json(issues);
}
};
/* ************************************************************************ */
export default { validate };The validate middleware is then used in routes that require validation:
import itemValidator from "./itemValidator";
itemRoutes.post(
"/api/items",
itemValidator.validate, itemActions.add
);
itemRoutes.put(
"/api/items/:itemId",
itemValidator.validate, itemActions.edit
);This step ensures that the data processed by your API is consistent, secure, and conforms to the expected model.
At this point, the Express foundation of your application is complete: modules, routes, actions, and validations are in place. In the next section, you will see how to connect this API to the React part of StartER to create a unified application.