Express en US - rocambille/start-express-react GitHub Wiki

Express is a minimalist and versatile web framework for server-side development. Here's a sample code sample to gauge your comfort level with the tool:

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 "hello, world!" message returned in an HTTP response, you've mastered the basics of the framework.

Otherwise, we recommend that you review the Express documentation before going any further. In particular, make sure you are comfortable with routing in Express.

Creating routes

In the proposed base code, the creation of the StartER one server uses the routes defined in the src/express/routes.ts file.

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-app as if it were 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.

Creating a module

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 aims to create distinct parts of an application with isolated functionalities. In StartER, our criterion for isolating functionalities is to group them by "theme."


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);

// ...

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. Give a chance to logics that are not yours!


Isolating 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;

A TypeScript word: To enable type inference on req and res objects, we recommend typing your action with Express's 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 };

Providing data access

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 can be a step in your prototyping. Our logic is that data access should 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:

Param converter

In StartER, we use Express's "param converter" mechanism to transform URL parameters into entity 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 { RequestHandler } from "express";

import itemRepository from "./itemRepository";

const convert: RequestHandler = (req, res, next) => {
  const item = itemRepository.read(req.params.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's Request type using declaration merging (see the manual).

import type { RequestHandler } from "express";

import itemRepository from "./itemRepository";

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

declare global {
  namespace Express {
    interface Request {
      item: Item; // Extends Request interface of Express
    }
  }
}

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

const convert: RequestHandler = (req, res, next) => {
  // ...
};

export default { convert };

To use the converter, we 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
};

Input validation

Data validation is crucial for any robust web application. In StartER, we use zod for input validation.

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) {
    res.status(400).json((err as ZodError).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
);
⚠️ **GitHub.com Fallback** ⚠️