# Fastify: Type safe with Type-Providers

In this article we will see how to automatically type Fastify routes via the JSON schema, using the Type-Provider: TypeBox.

> [click here to read the article in italian language](https://blog.manuelsalinardi.it/fastify-type-safe-con-i-type-providers)

## Pre-requirements

Node.js v20.13.1

## What are Type-Providers?

[Documentation: Type-Providers](https://fastify.dev/docs/latest/Reference/Type-Providers/)

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](https://github.com/sinclairzx81/typebox#validation).

## Let's create the Fastify project with Typescript

We open the terminal and type this command:

```bash
npx fastify-cli generate my-app --lang=ts
```

This command will generate a Fastify project with Typescript, using the utility [fastify-cli](https://github.com/fastify/fastify-cli).

## Project structure

At this point, we have a project with the following structure:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1718260761874/d8a079fb-480d-4cbc-9311-0b00c4270c9f.png align="center")

## Contents of the package.json generated

```json
{
  "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"
  }
}
```

## Let's install the package.json dependencies

Enter the folder of the newly created "my-app" project and install all the dependencies of the package.json with this command:

```bash
npm install
```

## Default route root

fastify-cli has already created 2 routes for us, we can find them inside the files:

1. src/routes/root.ts
    
2. src/routes/example/index.ts
    

Let's see the generated code for the root route:

```typescript
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'.

## Let's add our route

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.

```typescript
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1718347595519/7d91eea8-1871-489b-a628-44312d5915b7.png align="center")

## Typescript error

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.

## Let's install the type-provider-typebox library

```bash
npm i @fastify/type-provider-typebox
```

## Let's create the schema with Typebox

```typescript
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.

## Let's use the schema to generate the types for the route

```typescript
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:

* FastifyPluginAsync (type for plugin imported directly from 'fastify')
    

With the type:

* FastifyPluginAsyncTypebox (type for plugin imported from '@fastify/type-provider-typebox')
    

## Refactoring

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.

```typescript
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:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1719326501941/721ef45d-6663-49c1-b093-c668a6284e52.png align="center")

And also note that the return is no longer typed. Typescript allows us to do this:

```typescript
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.

## Route handler type

```typescript
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:

* We transformed the postUppercase function from this:
    

```typescript
async function postUppercase(request: FastifyRequest, reply: FastifyReply) {
    const body = request.body
    const text = body.text
    return { textResult: text.toUpperCase()} 
}
```

* To this:
    

```typescript
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:

```typescript
RouteHandlerTypebox<typeof schemaPostUppercase>
```

Let's see its definition:

```typescript
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
