Skip to content

Commit 2c16e94

Browse files
authored
Add Fastify Adapter 🎉 (#237)
1 parent 69f313a commit 2c16e94

34 files changed

Lines changed: 1335 additions & 6 deletions

.changeset/nine-tools-thank.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/fastify': minor
3+
---
4+
5+
Release initial fastify implementation

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"cSpell.words": [
3+
"fastify",
34
"openapi",
45
"solidjs",
56
"tanstack",

apps/docs/docs/fastify.mdx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { InstallTabs } from '@site/src/components/InstallTabs';
2+
3+
# Fastify Server
4+
5+
## Installation
6+
7+
<InstallTabs packageName="@ts-rest/fastify" />
8+
9+
## Usage
10+
11+
```typescript
12+
import * as fastify from 'fastify';
13+
import { initServer } from '@ts-rest/fastify';
14+
15+
const app = fastify();
16+
17+
const s = initServer();
18+
19+
const router = s.router(contract, {
20+
getPost: async ({ params: { id } }) => {
21+
const post = await prisma.post.findUnique({ where: { id } });
22+
23+
return {
24+
status: 200,
25+
body: post,
26+
};
27+
},
28+
createPost: async ({ body }) => {
29+
const post = await prisma.post.create({
30+
data: body,
31+
});
32+
33+
return {
34+
status: 201,
35+
body: post,
36+
};
37+
},
38+
});
39+
40+
s.registerRouter(contract, router, app);
41+
42+
const start = async () => {
43+
try {
44+
await app.listen({ port: 3000 });
45+
} catch (err) {
46+
app.log.error(err);
47+
process.exit(1);
48+
}
49+
};
50+
51+
start();
52+
```
53+
54+
`s.registerRouter` is a function that takes a contract, a corresponding router with implementations for each route and an fastify app, and it will
55+
create the corresponding fastify routes for each endpoint with the correct method, paths and middleware and attach them to your fastify app.
56+
57+
## Options
58+
59+
You can pass an optional options object as the last argument for `createfastifyEndpoints`.
60+
61+
```typescript
62+
type Options = {
63+
logInitialization?: boolean; // print route initialization logs to console
64+
jsonQuery?: boolean;
65+
responseValidation?: boolean;
66+
requestValidationErrorHandler?:
67+
| 'combined'
68+
| ((
69+
err: TsRestRequestValidationError,
70+
request: fastify.FastifyRequest,
71+
reply: fastify.FastifyReply
72+
) => void);
73+
};
74+
```
75+
76+
### Response Validation
77+
78+
To enable response parsing and validation, you can use the `validateResponses` option.
79+
If there is a corresponding response Zod schema defined in the contract for the returned status code, the response will be parsed and validated.
80+
If validation fails a `ResponseValidationError` will be thrown causing a 500 response to be returned.
81+
82+
```typescript
83+
createfastifyEndpoints(contract, router, app, {
84+
validateResponses: true,
85+
});
86+
```
87+
88+
### Request Validation Error Handling
89+
90+
The default functionality of handling request validation errors is `combined` which returns a 400 response with all validation errors in the body in this form.
91+
92+
```typescript
93+
{
94+
pathParameterErrors: z.ZodError | null;
95+
headerErrors: z.ZodError | null;
96+
queryParameterErrors: z.ZodError | null;
97+
bodyErrors: z.ZodError | null;
98+
}
99+
```
100+
101+
You can also pass a custom error handler function to the `requestValidationErrorHandler` option.
102+
103+
```typescript
104+
s.registerRouter(contract, router, app, {
105+
requestValidationErrorHandler: (err, req, res, next) => {
106+
// err is typed as ^ RequestValidationError
107+
return res.status(400).json({
108+
message: 'Validation failed',
109+
});
110+
},
111+
});
112+
```

apps/docs/docs/quickstart.mdx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,53 @@ const port = process.env.port || 3333;
210210
const server = app.listen(port, () => {
211211
console.log(`Listening at http://localhost:${port}`);
212212
});
213+
```
214+
215+
</TabItem>
216+
<TabItem value="fastify" label="Fastify">
217+
<InstallTabs packageName="@ts-rest/fastify" />
218+
<p>The fastify implementaton allows full type safety, offering; body parsing, query parsing, param parsing and full error handling</p>
219+
220+
```typescript
221+
// main.ts
222+
223+
const app = fastify();
224+
225+
const s = initServer();
226+
227+
const router = s.router(contract, {
228+
getPost: async ({ params: { id } }) => {
229+
const post = await prisma.post.findUnique({ where: { id } });
230+
231+
return {
232+
status: 200,
233+
body: post,
234+
};
235+
},
236+
createPost: async ({ body }) => {
237+
const post = await prisma.post.create({
238+
data: body,
239+
});
240+
241+
return {
242+
status: 201,
243+
body: post,
244+
};
245+
},
246+
});
247+
248+
s.registerRouter(contract, router, app);
249+
250+
const start = async () => {
251+
try {
252+
await app.listen({ port: 3000 });
253+
} catch (err) {
254+
app.log.error(err);
255+
process.exit(1);
256+
}
257+
};
258+
259+
start();
213260
```
214261

215262
</TabItem>

apps/docs/sidebars.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ const sidebars = {
8686
label: '@ts-rest/next',
8787
id: 'next',
8888
},
89+
{
90+
type: 'doc',
91+
label: '@ts-rest/fastify',
92+
id: 'fastify',
93+
},
8994
{
9095
type: 'category',
9196
label: '@ts-rest/express',

apps/docs/src/pages/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import express from '../../static/img/express.png';
1919
// @ts-ignore
2020
import nest from '../../static/img/nest.png';
2121
// @ts-ignore
22+
import fastify from '../../static/img/fastify.png';
23+
// @ts-ignore
2224
import openApi from '../../static/img/swagger.png';
2325
// @ts-ignore
2426
import vercel from '../../static/img/vercel.png';
@@ -223,6 +225,11 @@ export default function Home(): JSX.Element {
223225
description: 'Nest.js integration',
224226
image: nest,
225227
},
228+
{
229+
name: '@ts-rest/fastify',
230+
description: 'Fastify integration 🆕',
231+
image: fastify,
232+
},
226233
{
227234
name: '@ts-rest/open-api',
228235
description:

apps/docs/static/img/fastify.png

2.6 KB
Loading
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"extends": ["../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7+
"rules": {}
8+
},
9+
{
10+
"files": ["*.ts", "*.tsx"],
11+
"rules": {}
12+
},
13+
{
14+
"files": ["*.js", "*.jsx"],
15+
"rules": {}
16+
}
17+
]
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* eslint-disable */
2+
export default {
3+
displayName: 'example-fastify',
4+
preset: '../../jest.preset.js',
5+
globals: {
6+
'ts-jest': {
7+
tsconfig: '<rootDir>/tsconfig.spec.json',
8+
},
9+
},
10+
testEnvironment: 'node',
11+
transform: {
12+
'^.+\\.[tj]s$': 'ts-jest',
13+
},
14+
moduleFileExtensions: ['ts', 'js', 'html'],
15+
coverageDirectory: '../../coverage/apps/example-fastify',
16+
};

apps/example-fastify/project.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "example-fastify",
3+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "apps/example-fastify/src",
5+
"projectType": "application",
6+
"targets": {
7+
"build": {
8+
"executor": "@nrwl/webpack:webpack",
9+
"outputs": ["{options.outputPath}"],
10+
"options": {
11+
"target": "node",
12+
"compiler": "tsc",
13+
"outputPath": "dist/apps/example-fastify",
14+
"main": "apps/example-fastify/src/main.ts",
15+
"tsConfig": "apps/example-fastify/tsconfig.app.json",
16+
"assets": ["apps/example-fastify/src/assets"]
17+
},
18+
"configurations": {
19+
"production": {
20+
"optimization": true,
21+
"extractLicenses": true,
22+
"inspect": false
23+
}
24+
}
25+
},
26+
"serve": {
27+
"executor": "@nrwl/js:node",
28+
"options": {
29+
"buildTarget": "example-fastify:build"
30+
},
31+
"configurations": {
32+
"production": {
33+
"buildTarget": "example-fastify:build:production"
34+
}
35+
}
36+
},
37+
"lint": {
38+
"executor": "@nrwl/linter:eslint",
39+
"outputs": ["{options.outputFile}"],
40+
"options": {
41+
"lintFilePatterns": ["apps/example-fastify/**/*.ts"]
42+
}
43+
},
44+
"test": {
45+
"executor": "@nrwl/jest:jest",
46+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
47+
"options": {
48+
"jestConfig": "apps/example-fastify/jest.config.ts",
49+
"passWithNoTests": true
50+
}
51+
}
52+
},
53+
"tags": []
54+
}

0 commit comments

Comments
 (0)