-
-
Notifications
You must be signed in to change notification settings - Fork 141
feat(nest): implement contract in NestJS #480
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
Merged
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
ee5098d
wip
dinwwwh cc87bed
fix read me
dinwwwh 7f7f1fb
nestjs
dinwwwh 5e97340
wip
dinwwwh 665439e
wip
dinwwwh be236f0
docs
dinwwwh 4048377
fix naming
dinwwwh ed952c7
improve
dinwwwh f204e52
improve
dinwwwh 400be53
docs
dinwwwh 422e0d6
improve
dinwwwh 5a11cca
fix
dinwwwh a00806e
experimental warning
dinwwwh 9d8d712
wip
dinwwwh c8a2834
improve
dinwwwh 2cc4699
wip
dinwwwh 1192506
100% test coverage
dinwwwh 8a4e999
add missing zod dependency
dinwwwh ac6b06e
type tests
dinwwwh a42fcc9
Merge branch 'main' into feat/nest/init
dinwwwh b652589
update pnpm lock
dinwwwh 57d88d8
jsdocs
dinwwwh 4477631
fixed
dinwwwh 33917f5
alias
dinwwwh a3d1d25
improve
dinwwwh 3944e8d
gramma
dinwwwh 17fe6a7
improve
dinwwwh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| --- | ||
| title: Implement Contract in NestJS | ||
| description: Seamlessly implement oRPC contracts in your NestJS applications. | ||
| --- | ||
|
|
||
| # Implement Contract in NestJS | ||
|
|
||
| This guide explains how to easily implement [oRPC contract](/docs/contract-first/define-contract) within your [NestJS](https://nestjs.com/) application using `@orpc/nest`. | ||
|
|
||
| ::: warning | ||
| This feature is currently experimental and may be subject to breaking changes. | ||
| ::: | ||
|
|
||
| ## Installation | ||
|
|
||
| ::: code-group | ||
|
|
||
| ```sh [npm] | ||
| npm install @orpc/nest@latest | ||
| ``` | ||
|
|
||
| ```sh [yarn] | ||
| yarn add @orpc/nest@latest | ||
| ``` | ||
|
|
||
| ```sh [pnpm] | ||
| pnpm add @orpc/nest@latest | ||
| ``` | ||
|
|
||
| ```sh [bun] | ||
| bun add @orpc/nest@latest | ||
| ``` | ||
|
|
||
| ```sh [deno] | ||
| deno install npm:@orpc/nest@latest | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| ## Requirements | ||
|
|
||
| oRPC is an ESM-only library. Therefore, your NestJS application must be configured to support ESM modules. | ||
|
|
||
| 1. **Configure `tsconfig.json`**: with `"module": "NodeNext"` or a similar ESM-compatible option. | ||
|
|
||
| ```json | ||
| { | ||
| "compilerOptions": { | ||
| "module": "NodeNext", // <-- this is recommended | ||
| "strict": true // <-- this is recommended | ||
| // ... other options, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| 2. **Node.js Environment**: | ||
|
|
||
| - **Node.js 22+**: Recommended, as it allows `require()` of ESM modules natively. | ||
| - **Older Node.js versions**: Alternatively, use a bundler to compile ESM modules (including `@orpc/nest`) to CommonJS. | ||
|
|
||
| ::: warning | ||
| By default, NestJS bundler ([Webpack](https://webpack.js.org/) or [SWC](https://swc.rs/)) might not compile `node_modules`. You may need to adjust your bundler configs to include `@orpc/nest` for compilation. | ||
| ::: | ||
|
|
||
| ## Define Your Contract | ||
|
|
||
| Before implementation, define your oRPC contract. This process is consistent with the standard oRPC methodology. For detailed guidance, refer to the main [Contract-First guide](/docs/contract-first/define-contract). | ||
|
|
||
| ::: details Example Contract | ||
|
|
||
| ```ts | ||
| import { populateContractRouterPaths } from '@orpc/nest' | ||
| import { oc } from '@orpc/contract' | ||
| import { z } from 'zod' | ||
|
|
||
| export const PlanetSchema = z.object({ | ||
| id: z.number().int().min(1), | ||
| name: z.string(), | ||
| description: z.string().optional(), | ||
| }) | ||
|
|
||
| export const listPlanetContract = oc | ||
| .route({ | ||
| method: 'GET', | ||
| path: '/planets' // Path is required for NestJS implementation | ||
| }) | ||
| .input( | ||
| z.object({ | ||
| limit: z.number().int().min(1).max(100).optional(), | ||
| cursor: z.number().int().min(0).default(0), | ||
| }), | ||
| ) | ||
| .output(z.array(PlanetSchema)) | ||
|
|
||
| export const findPlanetContract = oc | ||
| .route({ | ||
| method: 'GET', | ||
| path: '/planets/{id}' // Path is required | ||
| }) | ||
| .input(PlanetSchema.pick({ id: true })) | ||
| .output(PlanetSchema) | ||
|
|
||
| export const createPlanetContract = oc | ||
| .route({ | ||
| method: 'POST', | ||
| path: '/planets' // Path is required | ||
| }) | ||
| .input(PlanetSchema.omit({ id: true })) | ||
| .output(PlanetSchema) | ||
|
|
||
| /** | ||
| * populateContractRouterPaths is completely optional, | ||
| * because the procedure's path is required for NestJS implementation. | ||
| * This utility automatically populates any missing paths | ||
| * Using the router's keys + `/`. | ||
| */ | ||
| export const contract = populateContractRouterPaths({ | ||
| planet: { | ||
| list: listPlanetContract, | ||
| find: findPlanetContract, | ||
| create: createPlanetContract, | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| ::: warning | ||
| For a contract to be implementable in NestJS using `@orpc/nest`, each contract **must** define a `path` in its `.route`. Omitting it will cause a build‑time error. | ||
| You can avoid this by using the `populateContractRouterPaths` utility to automatically fill in any missing paths. | ||
| ::: | ||
|
|
||
| ## Path Parameters | ||
|
|
||
| Aside from [oRPC Path Parameters](/docs/openapi/routing#path-parameters), regular NestJS route patterns still work out of the box. However, they are not standard in OpenAPI, so we recommend using oRPC Path Parameters exclusively. | ||
|
|
||
| ::: warning | ||
| [oRPC Path Parameter matching with slashes (/)](/docs/openapi/routing#path-parameters) does not work on the NestJS Fastify platform, because Fastify does not allow wildcard (`*`) aliasing in path parameters. | ||
| ::: | ||
|
|
||
| ## Implement Your Contract | ||
|
|
||
| ```ts | ||
| import { Implement, implement, ORPCError } from '@orpc/nest' | ||
|
|
||
| @Controller() | ||
| export class PlanetController { | ||
| /** | ||
| * Implement a standalone procedure | ||
| */ | ||
| @Implement(contract.planet.list) | ||
| list() { | ||
| return implement(contract.planet.list).handler(({ input }) => { | ||
| // Implement logic here | ||
|
|
||
| return [] | ||
| }) | ||
| } | ||
|
|
||
| /** | ||
| * Implement entire a contract | ||
| */ | ||
| @Implement(contract.planet) | ||
| planet() { | ||
| return { | ||
| list: implement(contract.planet.list).handler(({ input }) => { | ||
| // Implement logic here | ||
| return [] | ||
| }), | ||
| find: implement(contract.planet.find).handler(({ input }) => { | ||
| // Implement logic here | ||
| return { | ||
| id: 1, | ||
| name: 'Earth', | ||
| description: 'The planet Earth', | ||
| } | ||
| }), | ||
| create: implement(contract.planet.create).handler(({ input }) => { | ||
| // Implement logic here | ||
| return { | ||
| id: 1, | ||
| name: 'Earth', | ||
| description: 'The planet Earth', | ||
| } | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| // other handlers... | ||
| } | ||
| ``` | ||
|
|
||
| ::: info | ||
| The `@Implement` decorator functions similarly to NestJS built-in HTTP method decorators (e.g., `@Get`, `@Post`). Handlers decorated with `@Implement` are standard NestJS controller handlers and can leverage all NestJS features. | ||
| ::: | ||
|
|
||
| ## Body Parser | ||
|
|
||
| By default, NestJS parses request bodies for `application/json` and `application/x-www-form-urlencoded` content types. However: | ||
|
|
||
| - NestJS `urlencoded` parser does not support [Bracket Notation](/docs/openapi/bracket-notation) like in standard oRPC parsers. | ||
| - In some edge cases like uploading a file with `application/json` content type, the NestJS parser does not treat it as a file, instead it parses the body as a JSON string. | ||
|
|
||
| Therefore, we **recommend** disabling the NestJS body parser: | ||
|
|
||
| ```ts | ||
| import { NestFactory } from '@nestjs/core' | ||
| import { AppModule } from './app.module' | ||
|
|
||
| async function bootstrap() { | ||
| const app = await NestFactory.create(AppModule, { | ||
| bodyParser: false, // [!code highlight] | ||
| }) | ||
|
|
||
| await app.listen(process.env.PORT ?? 3000) | ||
| } | ||
| ``` | ||
|
|
||
| ::: info | ||
| oRPC will use NestJS parsed body when it's available, and only use the oRPC parser if the body is not parsed by NestJS. | ||
| ::: | ||
|
|
||
| ## Create a Type-Safe Client | ||
|
|
||
| When you implement oRPC contracts in NestJS using `@orpc/nest`, the resulting API endpoints are OpenAPI compatible. This allows you to use an OpenAPI-compatible client link, such as [OpenAPILink](/docs/openapi/client/openapi-link), to interact with your API in a type-safe way. | ||
|
|
||
| ```typescript | ||
| import type { JsonifiedClient } from '@orpc/openapi-client' | ||
| import type { ContractRouterClient } from '@orpc/contract' | ||
| import { createORPCClient } from '@orpc/client' | ||
| import { OpenAPILink } from '@orpc/openapi-client/fetch' | ||
|
|
||
| const link = new OpenAPILink(contract, { | ||
| url: 'http://localhost:3000', | ||
| headers: () => ({ | ||
| 'x-api-key': 'my-api-key', | ||
| }), | ||
| // fetch: <-- polyfill fetch if needed | ||
| }) | ||
|
|
||
| const client: JsonifiedClient<ContractRouterClient<typeof contract>> = createORPCClient(link) | ||
| ``` | ||
|
|
||
| ::: info | ||
| Please refer to the [OpenAPILink](/docs/openapi/client/openapi-link) documentation for more information on client setup and options. | ||
| ::: | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Hidden folders and files | ||
| .* | ||
| !.gitignore | ||
| !.*.example | ||
|
|
||
| # Common generated folders | ||
| logs/ | ||
| node_modules/ | ||
| out/ | ||
| dist/ | ||
| dist-ssr/ | ||
| build/ | ||
| coverage/ | ||
| temp/ | ||
|
|
||
| # Common generated files | ||
| *.log | ||
| *.log.* | ||
| *.tsbuildinfo | ||
| *.vitest-temp.json | ||
| vite.config.ts.timestamp-* | ||
| vitest.config.ts.timestamp-* | ||
|
|
||
| # Common manual ignore files | ||
| *.local | ||
| *.pem |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.