Skip to content
Go back

Fastify TypeScript Setup: High-Performance Node.js API Framework

Fastify TypeScript Setup: High-Performance Node.js API Framework

Fastify is one of the fastest Node.js web frameworks, designed for maximum performance with minimal overhead. This comprehensive guide covers setting up Fastify v5.6.0 with TypeScript for production-ready API development.

Why Choose Fastify?

Step 1: Project Setup

Initialize your Fastify TypeScript project:

# Create project directory
mkdir fastify-typescript-api
cd fastify-typescript-api

# Initialize package.json
npm init -y

# Install Fastify v5.6.0 and essential plugins
npm install fastify@5.6.0 @fastify/cors @fastify/helmet @fastify/rate-limit
npm install @fastify/jwt @fastify/cookie @fastify/multipart

# Install TypeScript development dependencies
npm install -D typescript@5.7.2 @types/node@22.10.2 tsx@4.19.2 nodemon@3.1.9
npm install -D @typescript-eslint/eslint-plugin@8.18.1 @typescript-eslint/parser@8.18.1
npm install -D prettier@3.4.2 eslint@9.17.0

Step 2: TypeScript Configuration

Configure TypeScript for optimal development experience:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "CommonJS",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "allowJs": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "removeComments": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@/types/*": ["src/types/*"],
      "@/routes/*": ["src/routes/*"],
      "@/plugins/*": ["src/plugins/*"],
      "@/utils/*": ["src/utils/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}tsconfig.json

Step 3: Application Structure

Create the main application file:

import Fastify, { FastifyInstance } from "fastify";
import cors from "@fastify/cors";
import helmet from "@fastify/helmet";
import rateLimit from "@fastify/rate-limit";

// Import routes
import userRoutes from "./routes/users";
import authRoutes from "./routes/auth";

export async function createServer(): Promise<FastifyInstance> {
  const server = Fastify({
    logger: {
      level: process.env.LOG_LEVEL || "info",
      transport:
        process.env.NODE_ENV === "development"
          ? {
              target: "pino-pretty",
              options: {
                colorize: true,
              },
            }
          : undefined,
    },
  });

  // Register security plugins
  await server.register(helmet, {
    global: true,
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        scriptSrc: ["'self'"],
        imgSrc: ["'self'", "data:", "https:"],
      },
    },
  });

  await server.register(cors, {
    origin:
      process.env.NODE_ENV === "production" ? ["https://yourdomain.com"] : true,
    credentials: true,
  });

  await server.register(rateLimit, {
    global: true,
    max: 100,
    timeWindow: "1 minute",
    addHeaders: {
      "x-ratelimit-limit": true,
      "x-ratelimit-remaining": true,
      "x-ratelimit-reset": true,
      "retry-after": true,
    },
  });

  // Register routes
  await server.register(userRoutes, { prefix: "/api/users" });
  await server.register(authRoutes, { prefix: "/api/auth" });

  // Health check endpoint
  server.get("/health", async (request, reply) => {
    return { status: "ok", timestamp: new Date().toISOString() };
  });

  return server;
}src/app.ts

Step 4: Route Handlers with Validation

Create type-safe route handlers:

import {
  FastifyInstance,
  FastifyPluginOptions,
  FastifyReply,
  FastifyRequest,
} from "fastify";

interface CreateUserRequest {
  Body: {
    name: string;
    email: string;
    age: number;
  };
}

interface GetUserRequest {
  Params: {
    id: string;
  };
}

const createUserSchema = {
  type: "object",
  required: ["name", "email", "age"],
  properties: {
    name: { type: "string", minLength: 1, maxLength: 100 },
    email: { type: "string", format: "email" },
    age: { type: "integer", minimum: 0, maximum: 150 },
  },
};

const getUserSchema = {
  type: "object",
  required: ["id"],
  properties: {
    id: { type: "string", pattern: "^[0-9]+$" },
  },
};

async function userRoutes(
  server: FastifyInstance,
  options: FastifyPluginOptions
) {
  // Create user
  server.post<CreateUserRequest>(
    "/",
    {
      schema: {
        body: createUserSchema,
        response: {
          201: {
            type: "object",
            properties: {
              id: { type: "string" },
              name: { type: "string" },
              email: { type: "string" },
              age: { type: "number" },
              createdAt: { type: "string" },
            },
          },
        },
      },
    },
    async (request: FastifyRequest<CreateUserRequest>, reply: FastifyReply) => {
      const { name, email, age } = request.body;

      // Simulate user creation
      const user = {
        id: Math.random().toString(36).substr(2, 9),
        name,
        email,
        age,
        createdAt: new Date().toISOString(),
      };

      reply.status(201).send(user);
    }
  );

  // Get user by ID
  server.get<GetUserRequest>(
    "/:id",
    {
      schema: {
        params: getUserSchema,
        response: {
          200: {
            type: "object",
            properties: {
              id: { type: "string" },
              name: { type: "string" },
              email: { type: "string" },
              age: { type: "number" },
              createdAt: { type: "string" },
            },
          },
          404: {
            type: "object",
            properties: {
              error: { type: "string" },
              message: { type: "string" },
            },
          },
        },
      },
    },
    async (request: FastifyRequest<GetUserRequest>, reply: FastifyReply) => {
      const { id } = request.params;

      // Simulate user lookup
      if (id === "404") {
        reply.status(404).send({
          error: "Not Found",
          message: "User not found",
        });
        return;
      }

      const user = {
        id,
        name: "John Doe",
        email: "john@example.com",
        age: 30,
        createdAt: new Date().toISOString(),
      };

      reply.send(user);
    }
  );

  // Get all users
  server.get("/", async (request, reply) => {
    const users = [
      {
        id: "1",
        name: "John Doe",
        email: "john@example.com",
        age: 30,
        createdAt: new Date().toISOString(),
      },
      {
        id: "2",
        name: "Jane Smith",
        email: "jane@example.com",
        age: 25,
        createdAt: new Date().toISOString(),
      },
    ];

    reply.send({ users, total: users.length });
  });
}

export default userRoutes;src/routes/users.ts

Step 5: Authentication Routes

Implement JWT-based authentication:

import {
  FastifyInstance,
  FastifyPluginOptions,
  FastifyReply,
  FastifyRequest,
} from "fastify";
import jwt from "@fastify/jwt";

interface LoginRequest {
  Body: {
    email: string;
    password: string;
  };
}

const loginSchema = {
  type: "object",
  required: ["email", "password"],
  properties: {
    email: { type: "string", format: "email" },
    password: { type: "string", minLength: 6 },
  },
};

async function authRoutes(
  server: FastifyInstance,
  options: FastifyPluginOptions
) {
  // Register JWT plugin
  await server.register(jwt, {
    secret: process.env.JWT_SECRET || "supersecret",
  });

  // Login endpoint
  server.post<LoginRequest>(
    "/login",
    {
      schema: {
        body: loginSchema,
        response: {
          200: {
            type: "object",
            properties: {
              token: { type: "string" },
              user: {
                type: "object",
                properties: {
                  id: { type: "string" },
                  email: { type: "string" },
                  name: { type: "string" },
                },
              },
            },
          },
          401: {
            type: "object",
            properties: {
              error: { type: "string" },
              message: { type: "string" },
            },
          },
        },
      },
    },
    async (request: FastifyRequest<LoginRequest>, reply: FastifyReply) => {
      const { email, password } = request.body;

      // Simple authentication check
      if (email === "admin@example.com" && password === "password123") {
        const user = {
          id: "1",
          email: "admin@example.com",
          name: "Admin User",
        };

        const token = server.jwt.sign({
          userId: user.id,
          email: user.email,
        });

        reply.send({ token, user });
      } else {
        reply.status(401).send({
          error: "Unauthorized",
          message: "Invalid credentials",
        });
      }
    }
  );

  // Protected route example
  server.get(
    "/profile",
    {
      onRequest: [server.authenticate],
    },
    async (request, reply) => {
      const user = {
        id: "1",
        email: "admin@example.com",
        name: "Admin User",
        profile: {
          bio: "System Administrator",
          avatar: "https://example.com/avatar.jpg",
        },
      };

      reply.send({ user });
    }
  );
}

export default authRoutes;src/routes/auth.ts

Step 6: Server Entry Point

Create the main server file:

import { createServer } from "./app";

const start = async () => {
  try {
    const server = await createServer();

    const port = parseInt(process.env.PORT || "3000", 10);
    const host = process.env.HOST || "0.0.0.0";

    await server.listen({ port, host });

    server.log.info(`🚀 Server running at http://${host}:${port}`);

    // Graceful shutdown
    const signals: NodeJS.Signals[] = ["SIGINT", "SIGTERM"];

    signals.forEach(signal => {
      process.on(signal, async () => {
        server.log.info(`Received ${signal}, shutting down gracefully`);

        try {
          await server.close();
          process.exit(0);
        } catch (error) {
          server.log.error("Error during shutdown:", error);
          process.exit(1);
        }
      });
    });
  } catch (error) {
    console.error("Error starting server:", error);
    process.exit(1);
  }
};

start();src/server.ts

Step 7: Package.json Scripts

Add development and build scripts:

{
  "name": "fastify-typescript-api",
  "version": "1.0.0",
  "description": "High-performance Fastify API with TypeScript",
  "main": "dist/server.js",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js",
    "lint": "eslint src/**/*.ts",
    "lint:fix": "eslint src/**/*.ts --fix",
    "format": "prettier --write src/**/*.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["fastify", "typescript", "api", "nodejs", "performance"],
  "author": "Webtrophy Team",
  "license": "MIT",
  "engines": {
    "node": ">=18.0.0"
  }
}package.json

Step 8: Environment Configuration

Create environment configuration:

# Server Configuration
NODE_ENV=development
PORT=3000
HOST=0.0.0.0
LOG_LEVEL=info

# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key

# Database Configuration (if using)
DATABASE_URL=postgresql://user:password@localhost:5432/fastify_db

# Rate Limiting
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=60000

# CORS Origins
CORS_ORIGINS=http://localhost:3000,https://yourdomain.com.env

Step 9: Docker Configuration

Create Docker setup for production:

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY tsconfig.json ./
COPY src ./src

RUN npm run build

FROM node:20-alpine AS production

WORKDIR /app

RUN addgroup -g 1001 -S nodejs
RUN adduser -S fastify -u 1001

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

RUN chown -R fastify:nodejs /app
USER fastify

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]Dockerfile
version: "3.8"

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3docker-compose.yml

Step 10: Testing Setup

Create test configuration:

import { createServer } from "../app";
import { FastifyInstance } from "fastify";

describe("Fastify App", () => {
  let server: FastifyInstance;

  beforeAll(async () => {
    server = await createServer();
    await server.ready();
  });

  afterAll(async () => {
    await server.close();
  });

  test("health check should return 200", async () => {
    const response = await server.inject({
      method: "GET",
      url: "/health",
    });

    expect(response.statusCode).toBe(200);
    expect(JSON.parse(response.payload)).toHaveProperty("status", "ok");
  });

  test("should create user successfully", async () => {
    const userData = {
      name: "Test User",
      email: "test@example.com",
      age: 25,
    };

    const response = await server.inject({
      method: "POST",
      url: "/api/users",
      payload: userData,
    });

    expect(response.statusCode).toBe(201);
    const result = JSON.parse(response.payload);
    expect(result).toHaveProperty("name", userData.name);
    expect(result).toHaveProperty("email", userData.email);
  });

  test("should authenticate user", async () => {
    const credentials = {
      email: "admin@example.com",
      password: "password123",
    };

    const response = await server.inject({
      method: "POST",
      url: "/api/auth/login",
      payload: credentials,
    });

    expect(response.statusCode).toBe(200);
    const result = JSON.parse(response.payload);
    expect(result).toHaveProperty("token");
    expect(result).toHaveProperty("user");
  });
});src/__tests__/app.test.ts

Performance Optimization Tips

  1. Use Schema Validation: Fastify’s built-in JSON Schema validation is extremely fast
  2. Enable Logging: Structured logging helps with debugging and monitoring
  3. Use Plugins Wisely: Only register plugins you actually need
  4. HTTP/2 Support: Enable HTTP/2 for better performance
  5. Connection Pooling: Use connection pools for databases
  6. Caching: Implement caching strategies for frequently accessed data
  7. Type Providers: Use TypeBox or JSON Schema to TypeScript for better type safety

Best Practices

Version Information

Your Fastify TypeScript application is now ready for high-performance API development with type safety, validation, and production-ready configuration!


Share this post on:

Previous Post
Drizzle ORM PostgreSQL: Type-Safe Database Management
Next Post
Building a REST API with Fastify Plugins for Validation and Authentication