Skip to content

Commit

Permalink
Feat: beforeRouting option (#1549)
Browse files Browse the repository at this point in the history
The feature you've been probably dreamed about.

Based on the discussion
#1547.

This can enable `createServer` to be a standard approach in many cases
where currently only the `attachRouting` one is working.

Example UI served by a third-party middleware on its own route.


![image](https://github.com/RobinTail/express-zod-api/assets/13189514/c5fdd283-f2dd-458a-a0e5-f2d543b19863)
  • Loading branch information
RobinTail authored Feb 6, 2024
1 parent 9240b34 commit e97165a
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 16 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"files": [
"./tools/*.ts",
"./tests/**/*.ts",
"./tsup.config.ts"
"./tsup.config.ts",
"./example/*.ts"
],
"rules": {
"import/no-extraneous-dependencies": "off"
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

## Version 16

### v16.7.0

- Introducing the `beforeRouting` feature for the `ServerConfig`:
- The new option accepts a function that receives the express `app` and a `logger` instance.
- That function runs after parsing the request but before processing the `Routing` of your API.
- But most importantly, it runs before the "Not Found Handler".
- The option enables the configuration of the third-party middlewares serving their own routes or establishing their
own routing besides your primary API when using the standard `createServer()` method.
- The option helps to avoid making a custom express app, the DIY approach using `attachRouting()` method.
- The option can also be used to connect additional request parsers, like `cookie-parser`.

```ts
import { createConfig } from "express-zod-api";
import ui from "swagger-ui-express";

const config = createConfig({
server: {
listen: 80,
beforeRouting: ({ app, logger }) => {
logger.info("Serving the API documentation at https://example.com/docs");
app.use("/docs", ui.serve, ui.setup(documentation));
},
},
});
```

### v16.6.2

- Internal method `Endpoint::_setSiblingMethods()` removed (since v8.4.1);
Expand Down
42 changes: 30 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,22 +309,37 @@ const endpointsFactory = defaultEndpointsFactory.addOptions({

## Using native express middlewares

You can connect any native `express` middleware that can be supplied to `express` method `app.use()`.
For this purpose the `EndpointsFactory` provides method `addExpressMiddleware()` and its alias `use()`.
There are also two optional features available: a provider of options and an error transformer for `ResultHandler`.
In case the error in middleware is not a `HttpError`, the `ResultHandler` will send the status `500`.
There are two ways of connecting the native express middlewares depending on their nature and your objective.

In case it's a middleware establishing and serving its own routes, or somehow globally modifying the behaviour, or
being an additional request parser (like `cookie-parser`), use the `beforeRouting` option.
However, it might be better to avoid `cors` here — [the library handles it on its own](#cross-origin-resource-sharing).

```typescript
import { createConfig } from "express-zod-api";
import ui from "swagger-ui-express";

const config = createConfig({
server: {
listen: 80,
beforeRouting: ({ app, logger }) => {
logger.info("Serving the API documentation at https://example.com/docs");
app.use("/docs", ui.serve, ui.setup(documentation));
},
},
});
```

In case you need a special processing of `request`, or to modify the `response` for selected endpoints, use the method
`addExpressMiddleware()` of `EndpointsFactory` (or its alias `use()`). The method has two optional features: a provider
of [options](#options) and an error transformer for adjusting the response status code.

```typescript
import { defaultEndpointsFactory } from "express-zod-api";
import cors from "cors";
import createHttpError from "http-errors";
import { auth } from "express-oauth2-jwt-bearer";

const simpleUsage = defaultEndpointsFactory.addExpressMiddleware(
cors({ credentials: true }),
);

const advancedUsage = defaultEndpointsFactory.use(auth(), {
const factory = defaultEndpointsFactory.use(auth(), {
provider: (req) => ({ auth: req.auth }), // optional, can be async
transformer: (err) => createHttpError(401, err.message), // optional
});
Expand Down Expand Up @@ -815,8 +830,8 @@ const routing: Routing = {

## Connect to your own express app

If you already have your own configured express application, or you find the library settings not enough,
you can connect the endpoints to your app or any express router, using `attachRouting()` method:
If you already have your own configured express application, or you find the library settings not enough, you can
connect the endpoints to your app or any express router using the `attachRouting()` method:

```typescript
import express from "express";
Expand All @@ -841,6 +856,9 @@ const routing: Routing = {}; // your endpoints go here
errors yourself. In this regard `attachRouting()` provides you with `notFoundHandler` which you can optionally connect
to your custom express app.

Besides that, if you're looking to include additional request parsers, or a middleware that establishes its own routes,
then consider using the `beforeRouting` [option in config instead](#using-native-express-middlewares).

# Special needs

## Different responses for different status codes
Expand Down
11 changes: 11 additions & 0 deletions example/config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import express from "express";
import { createConfig } from "../src";
import ui from "swagger-ui-express";
import yaml from "yaml";
import { readFileSync } from "node:fs";

const documentation = yaml.parse(
readFileSync("example/example.documentation.yaml", "utf-8"),
);

export const config = createConfig({
server: {
listen: 8090,
upload: true,
compression: true, // affects sendAvatarEndpoint
rawParser: express.raw(), // required for rawAcceptingEndpoint
beforeRouting: ({ app }) => {
// third-party middlewares serving their own routes or establishing their own routing besides the API
app.use("/docs", ui.serve, ui.setup(documentation));
},
},
cors: true,
logger: {
Expand Down
2 changes: 1 addition & 1 deletion example/example.documentation.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: Example API
version: 16.6.2
version: 16.7.0-beta1
paths:
/v1/user/retrieve:
get:
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "express-zod-api",
"version": "16.6.2",
"version": "16.7.0-beta1",
"description": "A Typescript library to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -137,6 +137,7 @@
"@types/http-errors": "^2.0.2",
"@types/node": "^20.8.4",
"@types/ramda": "^0.29.3",
"@types/swagger-ui-express": "^4.1.6",
"@types/triple-beam": "^1.3.2",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
Expand All @@ -159,6 +160,7 @@
"make-coverage-badge": "^1.2.0",
"mockdate": "^3.0.5",
"prettier": "3.2.5",
"swagger-ui-express": "^5.0.0",
"tsd": "^0.30.0",
"tsup": "^8.0.0",
"tsx": "^4.6.2",
Expand Down
13 changes: 13 additions & 0 deletions src/config-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ type CompressionOptions = Pick<
"threshold" | "level" | "strategy" | "chunkSize" | "memLevel"
>;

type AppExtension = (params: {
app: IRouter;
logger: AbstractLogger;
}) => void | Promise<void>;

export interface ServerConfig<TAG extends string = string>
extends CommonConfig<TAG> {
/** @desc Server configuration. */
Expand Down Expand Up @@ -124,6 +129,14 @@ export interface ServerConfig<TAG extends string = string>
* @link https://expressjs.com/en/4x/api.html#express.raw
* */
rawParser?: RequestHandler;
/**
* @desc A code to execute after parsing the request body but before processing the Routing of your API.
* @desc This can be a good place for express middlewares establishing their own routes.
* @desc It can help to avoid making a DIY solution based on the attachRouting() approach.
* @default undefined
* @example ({ app }) => { app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); }
* */
beforeRouting?: AppExtension;
};
/** @desc Enables HTTPS server as well. */
https?: {
Expand Down
3 changes: 3 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const createServer = async (config: ServerConfig, routing: Routing) => {
const { rootLogger, notFoundHandler, parserFailureHandler } =
await makeCommonEntities(config);
app.use(parserFailureHandler);
if (config.server.beforeRouting) {
await config.server.beforeRouting({ app, logger: rootLogger });
}
initRouting({ app, routing, rootLogger, config });
app.use(notFoundHandler);

Expand Down
7 changes: 6 additions & 1 deletion tests/unit/server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ describe("Server", () => {
expect(httpListenSpy).toHaveBeenCalledWith(port, expect.any(Function));
});

test("Should create server with custom JSON parser, logger and error handler", async () => {
test("Should create server with custom JSON parser, logger, error handler and beforeRouting", async () => {
const customLogger = winston.createLogger({ silent: true });
const infoMethod = vi.spyOn(customLogger, "info");
const port = givePort();
const configMock = {
server: {
listen: { port }, // testing Net::ListenOptions
jsonParser: vi.fn(),
beforeRouting: vi.fn(),
},
cors: true,
startupLogo: false,
Expand Down Expand Up @@ -114,6 +115,10 @@ describe("Server", () => {
expect(appMock.use).toHaveBeenCalledTimes(3);
expect(appMock.use.mock.calls[0][0]).toBe(configMock.server.jsonParser);
expect(configMock.errorHandler.handler).toHaveBeenCalledTimes(0);
expect(configMock.server.beforeRouting).toHaveBeenCalledWith({
app: appMock,
logger: customLogger,
});
expect(infoMethod).toHaveBeenCalledTimes(1);
expect(infoMethod).toHaveBeenCalledWith(`Listening`, { port });
expect(appMock.get).toHaveBeenCalledTimes(1);
Expand Down
20 changes: 20 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,14 @@
"@types/mime" "*"
"@types/node" "*"

"@types/swagger-ui-express@^4.1.6":
version "4.1.6"
resolved "https://registry.yarnpkg.com/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz#d0929e3fabac1a96a8a9c6c7ee8d42362c5cdf48"
integrity sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==
dependencies:
"@types/express" "*"
"@types/serve-static" "*"

"@types/triple-beam@^1.3.2":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
Expand Down Expand Up @@ -4089,6 +4097,18 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==

swagger-ui-dist@>=5.0.0:
version "5.11.2"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz#b423e820928df703586ff58f80b09ffcf2434e08"
integrity sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==

swagger-ui-express@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz#7a00a18dd909574cb0d628574a299b9ba53d4d49"
integrity sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==
dependencies:
swagger-ui-dist ">=5.0.0"

synckit@^0.8.6:
version "0.8.8"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7"
Expand Down

0 comments on commit e97165a

Please sign in to comment.