Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: beforeRouting option #1549

Merged
merged 14 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 the 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
35 changes: 25 additions & 10 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
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: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
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
Loading