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": "[email protected]",
"password": "securePassword123",
"name": "John Doe"
}Login
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "[email protected]",
"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("[email protected]", "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 |