Skip to content

A secure authentication & authorization starter template built with Fastify, TypeScript, and Prisma, following OWASP security best practices.

Notifications You must be signed in to change notification settings

obeim/fastify-auth-template

Repository files navigation

Fastify Auth Template (TypeScript + Prisma)

A secure authentication & authorization starter template built with Fastify, TypeScript, and Prisma, following OWASP security best practices. Designed as a boilerplate for small to mid-sized projects.

Table of Contents


Features

  • JWT Authentication
    • Access token stored in memory (15-minute expiry)
    • Refresh token stored in HTTP-only secure cookies (7-day expiry)
    • Automatic token rotation on refresh
  • Role-based Authorization
    • Built-in roles: USER, ADMIN, MODERATOR
    • Flexible route protection with role combinations
  • OWASP Security Standards
    • Secure cookie flags (HttpOnly, Secure, SameSite=Strict)
    • Token rotation on refresh (prevents token replay attacks)
    • CORS with strict origin & credentials
    • Input validation with TypeBox
    • Password hashing with bcrypt (10 rounds)
    • Helmet for secure HTTP headers
    • Rate limiting (100 requests/minute)
  • Developer Experience
    • Full TypeScript support with strict typing
    • Hot reload in development
    • ESLint + Husky for code quality
    • Comprehensive test suite (Unit, Integration, E2E)

Tech Stack

Technology Purpose
Fastify High-performance web framework
TypeScript Type safety and developer experience
Prisma Database ORM with type-safe queries
PostgreSQL Primary database
Vitest Testing framework
TypeBox Runtime type validation

Project Structure

fastify-auth-template/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ server.ts              # Application entry point
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   └── config.ts          # Environment configuration
β”‚   β”œβ”€β”€ controllers/
β”‚   β”‚   β”œβ”€β”€ auth.controller.ts # Authentication request handlers
β”‚   β”‚   └── users.controller.ts# User routes request handlers
β”‚   β”œβ”€β”€ plugins/
β”‚   β”‚   β”œβ”€β”€ auth.ts            # JWT authentication plugin
β”‚   β”‚   └── prisma.ts          # Prisma database plugin
β”‚   β”œβ”€β”€ routes/
β”‚   β”‚   β”œβ”€β”€ auth.ts            # Authentication routes
β”‚   β”‚   └── users.ts           # User routes (with role examples)
β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”œβ”€β”€ auth.service.ts    # Business logic for auth
β”‚   β”‚   └── auth.service.test.ts # Unit tests for auth service
β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”œβ”€β”€ auth.d.ts          # JWT payload type definitions
β”‚   β”‚   └── fastify.d.ts       # Fastify request/reply types
β”‚   └── validations/
β”‚       └── auth.ts            # TypeBox validation schemas
β”œβ”€β”€ __tests__/
β”‚   β”œβ”€β”€ helpers/
β”‚   β”‚   β”œβ”€β”€ test-app.ts        # Test application builder
β”‚   β”‚   └── test-database.ts   # Database utilities for tests
β”‚   β”œβ”€β”€ e2e/
β”‚   β”‚   β”œβ”€β”€ auth.e2e.test.ts   # End-to-end auth flow tests
β”‚   β”‚   └── users.e2e.test.ts  # End-to-end user flow tests
β”‚   └── integration/
β”‚       β”œβ”€β”€ auth.integration.test.ts  # Auth API integration tests
β”‚       └── users.integration.test.ts # User routes integration tests
β”œβ”€β”€ prisma/
β”‚   β”œβ”€β”€ schema.prisma          # Database schema
β”‚   └── migrations/            # Database migrations
β”œβ”€β”€ vitest.config.ts           # Test configuration
β”œβ”€β”€ tsconfig.json              # TypeScript configuration
β”œβ”€β”€ eslint.config.ts           # ESLint configuration
└── package.json               # Dependencies and scripts

Directory Breakdown

Directory Purpose
src/config/ Application configuration and environment variables
src/controllers/ HTTP request handlers (thin layer, delegates to services)
src/plugins/ Fastify plugins for cross-cutting concerns
src/routes/ Route definitions and middleware attachment
src/services/ Business logic layer (testable, framework-agnostic)
src/types/ TypeScript type definitions and declarations
src/validations/ Request/response validation schemas
__tests__/helpers/ Shared test utilities and fixtures
__tests__/e2e/ End-to-end tests (complete user flows)
__tests__/integration/ Integration tests (API endpoint testing)
prisma/ Database schema and migrations

Getting Started

Prerequisites

  • Node.js >= 18.x
  • pnpm (recommended) or npm
  • PostgreSQL >= 13.x
  • Docker (optional, for containerized development)

Installation

# Clone the repository
git clone <repository-url>
cd fastify-auth-template

# Install dependencies
pnpm install

# Generate Prisma client
pnpm prisma generate

Environment Variables

Create a .env file in the root directory:

# Server
PORT=3000
HOST=0.0.0.0
NODE_ENV=development

# Database
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"

# JWT
JWT_SECRET="your-super-secret-jwt-key-change-in-production"

# CORS (JSON array of allowed origins)
CORS_ALLOWED_ORIGINS='["http://localhost:3000", "http://localhost:5173"]'

⚠️ Important: Never commit .env to version control. Use strong, unique secrets in production.

Database Setup

# Run migrations
pnpm prisma migrate dev

# (Optional) Seed the database
pnpm prisma db seed

# View database in Prisma Studio
pnpm prisma studio

Development

# Start development server with hot reload
pnpm dev

# Build for production
pnpm build

# Start production server
pnpm start

# Type checking
pnpm check-types

# Linting
pnpm lint

API Endpoints

Authentication (/api/v1/auth)

Method Endpoint Description Auth Required
POST /register Register a new user No
POST /login Login and get tokens No
GET /refresh Refresh access token Cookie
GET /logout Logout and invalidate tokens Yes

User Routes (/api/v1)

Method Endpoint Description Required Role
GET /publicRoute Public endpoint None
GET /authRoute Authenticated users only Any authenticated
GET /adminRoute Admin only ADMIN
GET /moderatorRoute Moderator only MODERATOR
GET /moderatorAndAdminRoute Admin or Moderator ADMIN or MODERATOR

Request/Response Examples

Register

POST /api/v1/auth/register
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securePassword123",
  "name": "John Doe"
}

Login

POST /api/v1/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securePassword123"
}

# Response
{
  "user": { "id": 1, "name": "John Doe", "role": "USER" },
  "accessToken": "eyJhbGciOiJIUzI1NiIs..."
}
# + HttpOnly cookie: refreshToken

Access Protected Route

GET /api/v1/authRoute
Authorization: Bearer <accessToken>

Authentication Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Client    β”‚     β”‚   Server    β”‚     β”‚  Database   β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚                   β”‚                   β”‚
       β”‚ 1. POST /login    β”‚                   β”‚
       │──────────────────>β”‚                   β”‚
       β”‚                   β”‚ 2. Verify creds   β”‚
       β”‚                   │──────────────────>β”‚
       β”‚                   β”‚<──────────────────│
       β”‚                   β”‚                   β”‚
       β”‚ 3. Access Token   β”‚                   β”‚
       β”‚   + Refresh Cookieβ”‚                   β”‚
       β”‚<──────────────────│                   β”‚
       β”‚                   β”‚                   β”‚
       β”‚ 4. GET /protected β”‚                   β”‚
       β”‚   + Bearer Token  β”‚                   β”‚
       │──────────────────>β”‚                   β”‚
       β”‚                   β”‚ 5. Verify JWT     β”‚
       β”‚                   β”‚                   β”‚
       β”‚ 6. Response       β”‚                   β”‚
       β”‚<──────────────────│                   β”‚
       β”‚                   β”‚                   β”‚
       β”‚ 7. GET /refresh   β”‚                   β”‚
       β”‚   + Cookie        β”‚                   β”‚
       │──────────────────>β”‚                   β”‚
       β”‚                   β”‚ 8. Validate &     β”‚
       β”‚                   β”‚    Rotate Token   β”‚
       β”‚                   │──────────────────>β”‚
       β”‚                   β”‚<──────────────────│
       β”‚ 9. New Tokens     β”‚                   β”‚
       β”‚<──────────────────│                   β”‚
       β”‚                   β”‚                   β”‚

Token Specifications

Token Storage Expiry Purpose
Access Token Client memory 15 minutes API authentication
Refresh Token HTTP-only cookie 7 days Token renewal

Role-Based Authorization

Available Roles

enum UserRole {
  USER      // Default role for new registrations
  ADMIN     // Full administrative access
  MODERATOR // Limited administrative access
}

Protecting Routes

// Single role
fastify.get(
  "/admin",
  {
    preHandler: [fastify.authenticate, fastify.authorize([UserRole.ADMIN])],
  },
  handler
);

// Multiple roles (OR logic)
fastify.get(
  "/staff",
  {
    preHandler: [
      fastify.authenticate,
      fastify.authorize([UserRole.ADMIN, UserRole.MODERATOR]),
    ],
  },
  handler
);

// Any authenticated user
fastify.get(
  "/profile",
  {
    preHandler: [fastify.authenticate],
  },
  handler
);

Testing

Test Structure

__tests__/
β”œβ”€β”€ helpers/                    # Shared test utilities
β”‚   β”œβ”€β”€ test-app.ts            # Builds test Fastify instance
β”‚   └── test-database.ts       # Database seeding & cleanup
β”œβ”€β”€ e2e/                       # End-to-End Tests
β”‚   β”œβ”€β”€ auth.e2e.test.ts       # Complete auth user journeys
β”‚   └── users.e2e.test.ts      # Multi-user scenarios
└── integration/               # Integration Tests
    β”œβ”€β”€ auth.integration.test.ts   # Auth API endpoints
    └── users.integration.test.ts  # User route permissions

src/services/
└── auth.service.test.ts       # Unit tests (mocked dependencies)

Test Types

Type Location Purpose Database
Unit src/**/*.test.ts Test business logic in isolation Mocked
Integration __tests__/integration/ Test API endpoints Real (test DB)
E2E __tests__/e2e/ Test complete user flows Real (test DB)

Running Tests

# Run all tests
pnpm test

# Run tests in watch mode
pnpm test -- --watch

# Run specific test file
pnpm test -- auth.integration.test.ts

# Run with coverage
pnpm test -- --coverage

# Run only unit tests
pnpm test -- src/

# Run only integration tests
pnpm test -- __tests__/integration/

# Run only E2E tests
pnpm test -- __tests__/e2e/

Writing Tests

Unit Test Example (with mocks)

import { describe, it, expect, vi, beforeEach } from "vitest";
import AuthService from "./auth.service";

const prismaMock = {
  user: {
    findUnique: vi.fn(),
    create: vi.fn(),
  },
};

describe("AuthService", () => {
  let authService: AuthService;

  beforeEach(() => {
    vi.resetAllMocks();
    authService = new AuthService(fastifyMock as any);
  });

  it("should throw error for invalid credentials", async () => {
    prismaMock.user.findUnique.mockReturnValue(null);
    await expect(
      authService.loginUser("test@email.com", "pass")
    ).rejects.toThrow("Invalid credentials");
  });
});

Integration Test Example (real API calls)

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { buildTestApp, generateTestEmail } from "../helpers/test-app";

describe("Auth API", () => {
  let app: FastifyInstance;

  beforeAll(async () => {
    app = await buildTestApp();
  });

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

  it("should register a user", async () => {
    const response = await app.inject({
      method: "POST",
      url: "/api/v1/auth/register",
      payload: {
        email: generateTestEmail(),
        password: "password123",
        name: "Test User",
      },
    });

    expect(response.statusCode).toBe(201);
  });
});

E2E Test Example (complete flows)

describe("User Journey", () => {
  it("register β†’ login β†’ access protected β†’ logout", async () => {
    // 1. Register
    const register = await app.inject({
      /* ... */
    });
    expect(register.statusCode).toBe(201);

    // 2. Login
    const login = await app.inject({
      /* ... */
    });
    const { accessToken } = JSON.parse(login.payload);

    // 3. Access protected route
    const protected = await app.inject({
      method: "GET",
      url: "/api/v1/authRoute",
      headers: { authorization: `Bearer ${accessToken}` },
    });
    expect(protected.statusCode).toBe(200);

    // 4. Logout
    const logout = await app.inject({
      /* ... */
    });
    expect(logout.statusCode).toBe(200);
  });
});

Security Best Practices

This template implements several OWASP recommendations:

Practice Implementation
Secure Password Storage bcrypt with 10 salt rounds
JWT Security Short-lived access tokens, HTTP-only refresh cookies
Token Rotation Refresh tokens are rotated on each use
CORS Strict origin allowlist with credentials
HTTP Headers Helmet with CSP, CORP, and other security headers
Rate Limiting 100 requests per minute per IP
Input Validation TypeBox schema validation on all inputs
Cookie Security HttpOnly, Secure, SameSite=Strict

Production Checklist

  • Use strong, unique JWT_SECRET (32+ characters)
  • Set NODE_ENV=production
  • Configure proper CORS_ALLOWED_ORIGINS
  • Use HTTPS (required for Secure cookies)
  • Set up database connection pooling
  • Enable logging and monitoring
  • Review and adjust rate limits
  • Set up health checks

Project Guidelines

Code Organization Rules

  1. Controllers - Thin layer, only HTTP concerns (request/response)
  2. Services - Business logic, testable without HTTP context
  3. Plugins - Fastify decorators and hooks
  4. Routes - Route definitions with middleware attachment
  5. Validations - TypeBox schemas, no logic

Adding New Features

  1. New Route

    1. Create validation schema in src/validations/
    2. Create/update service in src/services/
    3. Create/update controller in src/controllers/
    4. Register route in src/routes/
    5. Add tests (unit + integration)
    
  2. New Role

    1. Add to UserRole enum in prisma/schema.prisma
    2. Run pnpm prisma migrate dev
    3. Update route preHandlers as needed
    4. Add E2E tests for role access
    

File Naming Conventions

Type Pattern Example
Controllers *.controller.ts auth.controller.ts
Services *.service.ts auth.service.ts
Routes *.ts (in routes/) auth.ts
Validations *.ts (in validations/) auth.ts
Unit Tests *.test.ts (co-located) auth.service.test.ts
Integration Tests *.integration.test.ts auth.integration.test.ts
E2E Tests *.e2e.test.ts auth.e2e.test.ts

About

A secure authentication & authorization starter template built with Fastify, TypeScript, and Prisma, following OWASP security best practices.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published