Fastify: Type safe with Type-Providers

Search for a command to run...

No comments yet. Be the first to comment.
How to launch Fastify server in TypeScript without compiling, thanks to new native TypeScript support in Node.js.

Step-by-Step Guide to Adapt Fastify Logger to Your Needs.

I've been programming for about 7 years and this is my first article writing, and hopefully the first of a long series.

In this article we will see how to automatically type Fastify routes via the JSON schema, using the Type-Provider: TypeBox.
Node.js v20.13.1
Type-Providers are a feature only for Fastify projects that use Typescript as a language.
There are different types of Type-Providers, for this article, we will use: TypeBox.
We open the terminal and type this command:
npx fastify-cli generate my-app --lang=ts
This command will generate a Fastify project with Typescript, using the utility fastify-cli.
At this point, we have a project with the following structure:

{
"name": "my-app",
"version": "1.0.0",
"description": "This project was bootstrapped with Fastify-CLI.",
"main": "app.ts",
"directories": {
"test": "test"
},
"scripts": {
"test": "npm run build:ts && tsc -p test/tsconfig.json && c8 node --test -r ts-node/register \"test/**/*.ts\"",
"start": "npm run build:ts && fastify start -l info dist/app.js",
"build:ts": "tsc",
"watch:ts": "tsc -w",
"dev": "npm run build:ts && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch:ts\" \"npm:dev:start\"",
"dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@fastify/autoload": "^5.0.0",
"@fastify/sensible": "^5.0.0",
"@fastify/type-provider-typebox": "^4.0.0",
"fastify": "^4.26.1",
"fastify-cli": "^6.2.1",
"fastify-plugin": "^4.0.0"
},
"devDependencies": {
"@types/node": "^20.4.4",
"c8": "^9.0.0",
"concurrently": "^8.2.2",
"fastify-tsconfig": "^2.0.0",
"ts-node": "^10.4.0",
"typescript": "^5.2.2"
}
}
Enter the folder of the newly created "my-app" project and install all the dependencies of the package.json with this command:
npm install
fastify-cli has already created 2 routes for us, we can find them inside the files:
src/routes/root.ts
src/routes/example/index.ts
Let's see the generated code for the root route:
import { FastifyPluginAsync } from "fastify"
const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (request, reply) {
return 'this is an example'
})
}
export default example;
This route defines an HTTP GET call that returns a response string: 'this is an example'.
To make an example as simple as possible, let's add a route that takes text as input and returns the text in uppercase as a response.
import { FastifyPluginAsync } from "fastify"
const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (request, reply) {
return 'this is an example'
})
fastify.post('/uppercase', async function (request, reply) {
const body = request.body
const text = body.text
return { textResult: text.toUpperCase()}
})
}
export default example;
We have added a POST type route, the route expects a single 'text' property in the body and returns an object with a single 'textResult' property.

At this point Typescript will generate an error because initially, the body object is of type unknown, so we need to type the body object.
We could type the body object directly with a Typescript interface. Still, since we will be using a schema to validate the route, we will let Typescript generate the types directly from our schema, to have a single source of truth.
npm i @fastify/type-provider-typebox
import { Type } from '@fastify/type-provider-typebox'
import { FastifyPluginAsync } from "fastify"
const example: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (request, reply) {
return 'this is an example'
})
fastify.post('/uppercase', {
schema: {
body: Type.Object({
text: Type.String(),
}),
response: {
200: Type.Object({
textResult: Type.String()
}),
}
}
}, async function (request, reply) {
const body = request.body
const text = body.text
return { textResult: text.toUpperCase()}
})
}
export default example;
Now we have created a schema that defines that:
If the customer does not pass exactly one object with a string-type 'text' property in the body, he will receive a 404 error
If the server does not return exactly one object with the string-type 'textResult' property inside, the client will receive a 500 error
But there is still a problem, the schema is right but Typescript still doesn't compile because request.body is still of type unknown.
import { Type, FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
const example: FastifyPluginAsyncTypebox = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (request, reply) {
return 'this is an example'
})
fastify.post('/uppercase', {
schema: {
body: Type.Object({
text: Type.String(),
}),
response: {
200: Type.Object({
textResult: Type.String()
}),
}
}
}, async function (request, reply) {
const body = request.body
const text = body.text
return { textResult: text.toUpperCase()}
})
}
export default example;
We just replaced the type:
With the type:
Now everything works, but personally, for better readability, I prefer to separate the function of the route and the schema from the declaration of the route itself.
import { Type, FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import { FastifyReply, FastifyRequest } from 'fastify'
const example: FastifyPluginAsyncTypebox = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (request, reply) {
return 'this is an example'
})
const schemaPostUppercase = {
body: Type.Object({
text: Type.String(),
}),
response: {
200: Type.Object({
textResult: Type.String()
}),
}
}
async function postUppercase(request: FastifyRequest, reply: FastifyReply) {
const body = request.body
const text = body.text
return { textResult: text.toUpperCase()}
}
fastify.post('/uppercase', { schema: schemaPostUppercase }, postUppercase)
}
export default example;
This way we separated the schema into the variable: schemaPostUppercase and the route handler into the function: postUppercase.
But now Typescript is no longer able to type the body and return of the function and generates the error:

And also note that the return is no longer typed. Typescript allows us to do this:
return { hello: text.toUpperCase()}
Returning the 'hello' property should generate a Typescript compilation error because it does not respect the schema which instead defines that only the 'textResult' property is present in the response.
This is because the route no longer takes types from the schema.
import { Type, FastifyPluginAsyncTypebox, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import { ContextConfigDefault, FastifyBaseLogger, FastifyInstance, FastifyReply, FastifyRequest, FastifySchema, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault, RouteGenericInterface } from 'fastify'
import { ResolveFastifyReplyReturnType } from 'fastify/types/type-provider'
type FastifyInstanceTypebox = FastifyInstance<
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression,
FastifyBaseLogger,
TypeBoxTypeProvider
>
type FastifyRequestTypebox<TSchema extends FastifySchema> = FastifyRequest<
RouteGenericInterface,
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
TSchema,
TypeBoxTypeProvider
>
type FastifyReplyTypebox<TSchema extends FastifySchema> = FastifyReply<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
RouteGenericInterface,
ContextConfigDefault,
TSchema,
TypeBoxTypeProvider
>
export type RouteHandlerTypebox<TSchema extends FastifySchema> = (
this: FastifyInstanceTypebox,
request: FastifyRequestTypebox<TSchema>,
reply: FastifyReplyTypebox<TSchema>
) => ResolveFastifyReplyReturnType<TypeBoxTypeProvider, TSchema, RouteGenericInterface>
const example: FastifyPluginAsyncTypebox = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (request, reply) {
return 'this is an example'
})
const schemaPostUppercase = {
body: Type.Object({
text: Type.String(),
}),
response: {
200: Type.Object({
textResult: Type.String()
}),
}
}
const postUppercase: RouteHandlerTypebox<typeof schemaPostUppercase> = async function (request, reply) {
const body = request.body
const text = body.text
return { textResult: text.toUpperCase()}
}
fastify.post('/uppercase', { schema: schemaPostUppercase }, postUppercase)
}
export default example;
Unfortunately, in order to separate the declaration of the 'postUppercase' function from the route definition, we have to create its type manually.
For simplicity, I have defined all the types within the same file, but in a real project, you generally define the types in a separate file and import them into the files in which they are used.
Let's see this process step by step:
async function postUppercase(request: FastifyRequest, reply: FastifyReply) {
const body = request.body
const text = body.text
return { textResult: text.toUpperCase()}
}
const postUppercase: RouteHandlerTypebox<typeof schemaPostUppercase> = async function (request, reply) {
const body = request.body
const text = body.text
return { textResult: text.toUpperCase()}
}
postUppercase becomes from a function to a variable that contains an anonymous function, this syntax allows us to define the types for the parameters, the return, and the context (this) of the function with a single type:
RouteHandlerTypebox<typeof schemaPostUppercase>
Let's see its definition:
export type RouteHandlerTypebox<TSchema extends FastifySchema> = (
this: FastifyInstanceTypebox,
request: FastifyRequestTypebox<TSchema>,
reply: FastifyReplyTypebox<TSchema>
) => ResolveFastifyReplyReturnType<TypeBoxTypeProvider, TSchema, RouteGenericInterface>
In the RouteHandlerTypebox type, we are going to define types for:
this: the context of the function i.e. when you want to access the 'fastify' object via 'this'
request: the first parameter of the function
reply: the second parameter of the function
function return: ResolveFastifyReplyReturnType
Manually typing with the schema: request, reply, and the return of the function, which takes the generic type: TSchema.
Manuel Salinardi