How to Setup a NodeJS API with Fastify (TypeScript + runtime type support)

Gökhan Koç
4 min readNov 18, 2024

--

Building APIs can be tedious, especially when you need to maintain separate type definitions, validation schemas, and API documentation. What if you could write your types once and get everything else automatically? In this article, I’ll show you how to set up a NodeJS API using Fastify that automatically generates Swagger documentation from your TypeScript types.

TL;DR

I’ve created a CLI tool that sets up everything for you:

npx fastify-create my-api
cd my-api
npm install
npm run dev

Check out the GitHub repository for the complete template.

What’s Included?

  • Fastify with TypeScript
  • TypeBox for runtime type validation
  • Swagger UI for API documentation
  • Pre-configured TypeScript settings
  • Example endpoints with full type safety

Why TypeBox?

TypeBox is a crucial piece in our type-safe API puzzle, offering several significant advantages:

1. Single Source of Truth

Without TypeBox, you’d need to:

  • Define TypeScript interfaces for type checking
  • Define JSON Schema for runtime validation
  • Write Swagger/OpenAPI definitions for documentation

This leads to three separate definitions that need to be kept in sync. With TypeBox, you define your types once, and you get all three:

// Single definition that provides:
// 1. TypeScript types
// 2. Runtime validation
// 3. Swagger documentation
const UserSchema = Type.Object({
id: Type.Number(),
email: Type.String({ format: 'email' }),
age: Type.Number({ minimum: 0 }),
roles: Type.Array(Type.String())
});

// TypeScript type automatically derived
type User = Static<typeof UserSchema>;

2. Automatic Swagger Generation

TypeBox schemas are automatically converted to OpenAPI specifications. For example:

const OrderSchema = Type.Object({
id: Type.Number(),
items: Type.Array(Type.Object({
productId: Type.Number(),
quantity: Type.Integer({ minimum: 1 })
})),
status: Type.Union([
Type.Literal('pending'),
Type.Literal('completed'),
Type.Literal('cancelled')
])
});

This automatically generates the following Swagger/OpenAPI definition:

{
"type": "object",
"properties": {
"id": { "type": "number" },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"productId": { "type": "number" },
"quantity": {
"type": "integer",
"minimum": 1
}
}
}
},
"status": {
"enum": ["pending", "completed", "cancelled"]
}
}
}

3. Runtime Type Safety

TypeBox doesn’t just stop at compile-time checking. It generates JSON Schema validators that ensure your runtime data matches your types:

// Your type and validation schema in one
const LoginSchema = Type.Object({
email: Type.String({ format: 'email' }),
password: Type.String({ minLength: 8 })
});

fastify.post('/login', {
schema: {
body: LoginSchema, // Validates request body
response: { // Validates response
200: Type.Object({
token: Type.String()
})
}
}
}, async (request) => {
// request.body is fully typed
// Runtime validation is automatic
// Swagger docs are generated
});

4. Rich Type System

TypeBox provides a rich set of types that map directly to OpenAPI features:

const AdvancedSchema = Type.Object({
// String with pattern
zipCode: Type.String({ pattern: '^[0-9]{5}$' }),

// Enum values
role: Type.Enum({ Admin: 'admin', User: 'user' }),

// Optional fields
metadata: Type.Optional(Type.Object({
tags: Type.Array(Type.String())
})),

// Union types
status: Type.Union([
Type.Literal('active'),
Type.Literal('inactive')
])
});

5. IDE Integration

Because TypeBox works with TypeScript’s type system, you get excellent IDE support:

  • Autocomplete suggestions
  • Type checking
  • Inline documentation
  • Refactoring support

Step-by-Step Setup

1. Project Structure

my-api/
├── api/
│ ├── server.ts # Fastify server configuration
│ ├── routes.ts # API routes
│ └── generateOpenApi.ts
├── types/
│ ├── ErrorResponse.ts
│ ├── ExampleRequest.ts
│ └── ExampleResponse.ts
└── tsconfig.json

2. Server Setup (server.ts)

import fastify from "fastify";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import swagger from "@fastify/swagger";
import swaggerUi from "@fastify/swagger-ui";

export async function buildServer() {
const server = fastify({
logger: true,
}).withTypeProvider<TypeBoxTypeProvider>();

await server.register(swagger, {
openapi: {
info: {
title: "Fastify Template API",
description: "API documentation using Swagger",
version: "1.0.0",
},
servers: [{ url: "http://localhost:3000" }],
},
});

await server.register(swaggerUi, {
routePrefix: "/docs",
uiConfig: {
docExpansion: "full",
deepLinking: false,
},
});

await server.register(import("./routes"));
return server;
}

3. Type Definitions

// types/ExampleRequest.ts
import { Type } from "@sinclair/typebox";

export const ExampleRequestSchema = Type.Object({
name: Type.String(),
email: Type.String({ format: 'email' }),
age: Type.Number({ minimum: 0 }),
});

export type ExampleRequest = Static<typeof ExampleRequestSchema>;

4. Routes with Type Safety

import { FastifyPluginAsync } from "fastify";
import { Type } from "@sinclair/typebox";
import { ExampleRequestSchema } from "../types/ExampleRequest";

const routes: FastifyPluginAsync = async (fastify) => {
fastify.post<{
Body: ExampleRequest;
Reply: ExampleResponse | ErrorResponse;
}>(
"/example",
{
schema: {
description: "Example endpoint",
body: ExampleRequestSchema,
response: {
200: ExampleResponseSchema,
400: ErrorResponseSchema,
500: ErrorResponseSchema,
},
},
},
async (request, reply) => {
try {
// Your logic here
return { success: true, data: request.body };
} catch (error) {
reply.status(500).send({
statusCode: 500,
error: "Internal Server Error",
message: "An unexpected error occurred",
});
}
}
);
};

Using the Template

The easiest way to get started is using our CLI tool:

npx fastify-create my-api
cd my-api
npm install
npm run dev

Visit http://localhost:3000/docs to see your API documentation.

Conclusion
This setup provides a solid foundation for building type-safe APIs with Fastify. The combination of TypeBox and Swagger ensures both runtime type safety and excellent API documentation.

The template is open source and available on GitHub. Feel free to contribute or raise issues if you have suggestions for improvements!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response