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.
- Features
- Tech Stack
- Project Structure
- Getting Started
- Development
- API Endpoints
- Authentication Flow
- Role-Based Authorization
- Testing
- Security Best Practices
- Project Guidelines
- Contributing
- 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
- Built-in roles:
- 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)
- Secure cookie flags (
- Developer Experience
- Full TypeScript support with strict typing
- Hot reload in development
- ESLint + Husky for code quality
- Comprehensive test suite (Unit, Integration, E2E)
| 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 |
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 | 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 |
- Node.js >= 18.x
- pnpm (recommended) or npm
- PostgreSQL >= 13.x
- Docker (optional, for containerized development)
# Clone the repository
git clone <repository-url>
cd fastify-auth-template
# Install dependencies
pnpm install
# Generate Prisma client
pnpm prisma generateCreate 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.envto version control. Use strong, unique secrets in production.
# Run migrations
pnpm prisma migrate dev
# (Optional) Seed the database
pnpm prisma db seed
# View database in Prisma Studio
pnpm prisma studio# 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| 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 |
| 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 |
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: refreshTokenAccess Protected Route
GET /api/v1/authRoute
Authorization: Bearer <accessToken>βββββββββββββββ βββββββββββββββ βββββββββββββββ
β 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 | Storage | Expiry | Purpose |
|---|---|---|---|
| Access Token | Client memory | 15 minutes | API authentication |
| Refresh Token | HTTP-only cookie | 7 days | Token renewal |
enum UserRole {
USER // Default role for new registrations
ADMIN // Full administrative access
MODERATOR // Limited administrative access
}// 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
);__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)
| 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) |
# 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/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);
});
});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 |
- Use strong, unique
JWT_SECRET(32+ characters) - Set
NODE_ENV=production - Configure proper
CORS_ALLOWED_ORIGINS - Use HTTPS (required for
Securecookies) - Set up database connection pooling
- Enable logging and monitoring
- Review and adjust rate limits
- Set up health checks
- Controllers - Thin layer, only HTTP concerns (request/response)
- Services - Business logic, testable without HTTP context
- Plugins - Fastify decorators and hooks
- Routes - Route definitions with middleware attachment
- Validations - TypeBox schemas, no logic
-
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) -
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
| 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 |