From 2f3cc62b4482339db18eb5bf6fdae93e7bfc5272 Mon Sep 17 00:00:00 2001 From: unteifu <58165539+unteifu@users.noreply.github.com> Date: Fri, 3 Jan 2025 21:51:08 +0000 Subject: [PATCH 1/3] feat(server): added express adapter --- packages/server/package.json | 7 ++++++ .../src/adapters/express/composite-handler.ts | 19 +++++++++++++++ packages/server/src/adapters/express/index.ts | 3 +++ .../server/src/adapters/express/middleware.ts | 24 +++++++++++++++++++ packages/server/src/adapters/express/types.ts | 24 +++++++++++++++++++ playgrounds/expressjs/src/main.ts | 24 +++++++------------ pnpm-lock.yaml | 3 +++ 7 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 packages/server/src/adapters/express/composite-handler.ts create mode 100644 packages/server/src/adapters/express/index.ts create mode 100644 packages/server/src/adapters/express/middleware.ts create mode 100644 packages/server/src/adapters/express/types.ts diff --git a/packages/server/package.json b/packages/server/package.json index c0556db..e16c2ed 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -30,6 +30,11 @@ "import": "./dist/node.js", "default": "./dist/node.js" }, + "./express": { + "types": "./dist/src/adapters/express/index.d.ts", + "import": "./dist/express.js", + "default": "./dist/express.js" + }, "./🔒/*": { "types": "./dist/src/*.d.ts" } @@ -39,6 +44,7 @@ ".": "./src/index.ts", "./fetch": "./src/adapters/fetch/index.ts", "./node": "./src/adapters/node/index.ts", + "./express": "./src/adapters/express/index.ts", "./🔒/*": { "types": "./src/*.ts" } @@ -59,6 +65,7 @@ "@orpc/shared": "workspace:*" }, "devDependencies": { + "@types/express": "^5.0.0", "zod": "^3.24.1" } } diff --git a/packages/server/src/adapters/express/composite-handler.ts b/packages/server/src/adapters/express/composite-handler.ts new file mode 100644 index 0000000..bb4e40b --- /dev/null +++ b/packages/server/src/adapters/express/composite-handler.ts @@ -0,0 +1,19 @@ +import type { IncomingMessage, ServerResponse } from 'node:http' +import type { Context } from '../../types' +import type { ConditionalRequestHandler, RequestHandler, RequestOptions } from './types' + +export class CompositeHandler implements RequestHandler { + constructor( + private readonly handlers: ConditionalRequestHandler[], + ) {} + + async handle(req: IncomingMessage, res: ServerResponse, ...opt: [options: RequestOptions] | (undefined extends T ? [] : never)): Promise { + for (const handler of this.handlers) { + if (handler.condition(req)) { + return handler.handle(req, res, ...opt) + } + } + + res.statusCode = 404 + } +} diff --git a/packages/server/src/adapters/express/index.ts b/packages/server/src/adapters/express/index.ts new file mode 100644 index 0000000..2f7a6ba --- /dev/null +++ b/packages/server/src/adapters/express/index.ts @@ -0,0 +1,3 @@ +export * from './composite-handler' +export * from './middleware' +export * from './types' diff --git a/packages/server/src/adapters/express/middleware.ts b/packages/server/src/adapters/express/middleware.ts new file mode 100644 index 0000000..2f82a42 --- /dev/null +++ b/packages/server/src/adapters/express/middleware.ts @@ -0,0 +1,24 @@ +import type { ConditionalRequestHandler, RequestHandler, RequestOptions } from '@orpc/server/node' +import type { NextFunction, Request, Response } from 'express' +import type { Context } from '../../types' +import { CompositeHandler } from '@orpc/server/node' + +export function expressAdapter( + handlers: (ConditionalRequestHandler | RequestHandler)[], + options?: Partial>, +) { + const compositeHandler = new CompositeHandler(handlers as ConditionalRequestHandler[]) + + return (req: Request, res: Response, next: NextFunction) => { + try { + compositeHandler.handle(req, res, ...(options ? [options as RequestOptions] : [undefined as any])) + + if (res.statusCode === 404) { + next() + } + } + catch (error) { + next(error) + } + } +} diff --git a/packages/server/src/adapters/express/types.ts b/packages/server/src/adapters/express/types.ts new file mode 100644 index 0000000..faad3fd --- /dev/null +++ b/packages/server/src/adapters/express/types.ts @@ -0,0 +1,24 @@ +/// + +import type { RequestOptions as BaseRequestOptions } from '@mjackson/node-fetch-server' +import type { HTTPPath } from '@orpc/contract' +import type { Promisable } from '@orpc/shared' +import type { IncomingMessage, ServerResponse } from 'node:http' +import type { Context, WithSignal } from '../../types' + +export type RequestOptions = + & BaseRequestOptions + & WithSignal + & { prefix?: HTTPPath } + & (undefined extends T ? { context?: T } : { context: T }) + & { + beforeSend?: (response: Response, context: T) => Promisable + } + +export interface RequestHandler { + handle: (req: IncomingMessage, res: ServerResponse, ...opt: [options: RequestOptions] | (undefined extends T ? [] : never)) => void +} + +export interface ConditionalRequestHandler extends RequestHandler { + condition: (request: IncomingMessage) => boolean +} diff --git a/playgrounds/expressjs/src/main.ts b/playgrounds/expressjs/src/main.ts index 4e401af..3e59b29 100644 --- a/playgrounds/expressjs/src/main.ts +++ b/playgrounds/expressjs/src/main.ts @@ -1,6 +1,7 @@ import { OpenAPIGenerator } from '@orpc/openapi' import { OpenAPIServerHandler } from '@orpc/openapi/node' -import { CompositeHandler, ORPCHandler } from '@orpc/server/node' +import { expressAdapter } from '@orpc/server/express' +import { ORPCHandler } from '@orpc/server/node' import { ZodCoercer, ZodToJsonSchemaConverter } from '@orpc/zod' import express from 'express' import { router } from './router' @@ -17,23 +18,14 @@ const openAPIHandler = new OpenAPIServerHandler(router, { }, }) -const orpcHandler = new ORPCHandler(router, { - onError: ({ error }) => { - console.error(error) - }, -}) - -const compositeHandler = new CompositeHandler([openAPIHandler, orpcHandler]) +const orpcHandler = new ORPCHandler(router) -app.all('/api/*', (req, res) => { - const context = req.headers.authorization - ? { user: { id: 'test', name: 'John Doe', email: 'john@doe.com' } } - : {} +app.use(expressAdapter([orpcHandler, openAPIHandler], { + prefix: '/api', +})) - return compositeHandler.handle(req, res, { - prefix: '/api', - context, - }) +app.use((req, res, next) => { + res.status(404).send('Not Found') }) const openAPIGenerator = new OpenAPIGenerator({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e1e301..7d713c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,6 +331,9 @@ importers: specifier: workspace:* version: link:../shared devDependencies: + '@types/express': + specifier: ^5.0.0 + version: 5.0.0 zod: specifier: ^3.24.1 version: 3.24.1 From b34e34a655b67b8e537bf2062d116d02aca215e6 Mon Sep 17 00:00:00 2001 From: unteifu <58165539+unteifu@users.noreply.github.com> Date: Fri, 3 Jan 2025 22:05:57 +0000 Subject: [PATCH 2/3] feat(docs): updated integration docs to reflect changes --- .../content/docs/server/integrations.mdx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/content/content/docs/server/integrations.mdx b/apps/content/content/docs/server/integrations.mdx index b5a3a88..81467e2 100644 --- a/apps/content/content/docs/server/integrations.mdx +++ b/apps/content/content/docs/server/integrations.mdx @@ -67,24 +67,26 @@ server.listen(3000, () => { import express from 'express' import { ORPCHandler, CompositeHandler } from '@orpc/server/node' import { OpenAPIServerlessHandler } from '@orpc/openapi/node' +import { expressAdapter } from '@orpc/server/express' import { router } from 'examples/server' import { ZodCoercer } from '@orpc/zod' -const openapiHandler = new OpenAPIServerlessHandler(router, { +const app = express() + +const openAPIHandler = new OpenAPIServerlessHandler(router, { schemaCoercers: [ new ZodCoercer(), ], }) + const orpcHandler = new ORPCHandler(router) -const compositeHandler = new CompositeHandler([openapiHandler, orpcHandler]) -const app = express() +app.use(expressAdapter([orpcHandler, openAPIHandler], { + prefix: '/api', +})) -app.all('/api/*', (req, res) => { - return compositeHandler.handle(req, res, { - context: {}, - prefix: '/api', - }) +app.use((req, res, next) => { + res.status(404).send('Not Found') }) app.listen(3000, () => { From d668f25d194c0ec32fa3bf6b7bc22618fb81ce7c Mon Sep 17 00:00:00 2001 From: unteifu <58165539+unteifu@users.noreply.github.com> Date: Sat, 4 Jan 2025 16:27:21 +0000 Subject: [PATCH 3/3] feat(server): improved express handler performance --- .../src/adapters/express/composite-handler.ts | 13 +++++++++++- .../server/src/adapters/express/middleware.ts | 20 +++++++++---------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/server/src/adapters/express/composite-handler.ts b/packages/server/src/adapters/express/composite-handler.ts index bb4e40b..3725591 100644 --- a/packages/server/src/adapters/express/composite-handler.ts +++ b/packages/server/src/adapters/express/composite-handler.ts @@ -8,7 +8,18 @@ export class CompositeHandler implements RequestHandler { ) {} async handle(req: IncomingMessage, res: ServerResponse, ...opt: [options: RequestOptions] | (undefined extends T ? [] : never)): Promise { - for (const handler of this.handlers) { + const len = this.handlers.length + const handlers = this.handlers + + if (len > 0) { + const handler = handlers[0]! + if (handler.condition(req)) { + return handler.handle(req, res, ...opt) + } + } + + for (let i = 1; i < len; i++) { + const handler = handlers[i]! if (handler.condition(req)) { return handler.handle(req, res, ...opt) } diff --git a/packages/server/src/adapters/express/middleware.ts b/packages/server/src/adapters/express/middleware.ts index 2f82a42..08e23cd 100644 --- a/packages/server/src/adapters/express/middleware.ts +++ b/packages/server/src/adapters/express/middleware.ts @@ -1,7 +1,7 @@ -import type { ConditionalRequestHandler, RequestHandler, RequestOptions } from '@orpc/server/node' +import type { ConditionalRequestHandler, RequestHandler, RequestOptions } from '@orpc/server/express' import type { NextFunction, Request, Response } from 'express' import type { Context } from '../../types' -import { CompositeHandler } from '@orpc/server/node' +import { CompositeHandler } from '@orpc/server/express' export function expressAdapter( handlers: (ConditionalRequestHandler | RequestHandler)[], @@ -10,15 +10,13 @@ export function expressAdapter( const compositeHandler = new CompositeHandler(handlers as ConditionalRequestHandler[]) return (req: Request, res: Response, next: NextFunction) => { - try { - compositeHandler.handle(req, res, ...(options ? [options as RequestOptions] : [undefined as any])) - - if (res.statusCode === 404) { + Promise.resolve() + .then(() => + compositeHandler.handle(req, res, ...(options ? [options as RequestOptions] : [undefined as any])), + ) + .then(() => { next() - } - } - catch (error) { - next(error) - } + }) + .catch(next) } }