@a_jackie_z/fastify
v1.4.2
Published
A collection of Fastify plugins and utilities for building robust web applications.
Downloads
1,380
Readme
@a_jackie_z/fastify
A collection of Fastify plugins and utilities for building robust web applications with built-in support for dynamic JWT authentication, rate limiting, Swagger documentation, and Zod validation.
Installation
# For server-side Fastify applications
npm install @a_jackie_z/fastify
# For frontend applications (types and schemas only)
npm install @a_jackie_z/fastify-typesNote: If you're building a frontend application (React, Vue, etc.) and only need API types and response schemas, install
@a_jackie_z/fastify-typesinstead. It's frontend-compatible and has no Node.js dependencies.
Quick Start
Basic Setup
import { createFastify, runFastify } from '@a_jackie_z/fastify'
import { formatSuccess } from '@a_jackie_z/fastify-types'
import { z } from 'zod'
const app = await createFastify()
app.route({
method: 'GET',
url: '/hello',
schema: {
response: {
200: z.object({
status: z.number(),
success: z.boolean(),
data: z.object({
message: z.string(),
}),
}),
},
},
handler: async (_request, reply) => {
return reply.send(formatSuccess(200, { message: 'Hello World!' }))
},
})
await runFastify(app, '0.0.0.0', 3000)Package Architecture
This package is split into two complementary packages for better frontend compatibility:
@a_jackie_z/fastify (Server Package)
For: Node.js server applications Includes:
- Fastify server creation and configuration (
createFastify,runFastify) - JWT service with token generation/verification (
FastifyJwtService) - Server-side crypto utilities (
generateId,generateSessionToken) - Fastify plugins and middleware
- Server-specific types (
CreateFastifyOptions,FastifyServer)
@a_jackie_z/fastify-types (Types Package)
For: Frontend applications (React, Vue, etc.) and API contracts Includes:
- Response types (
SuccessResponse,ErrorResponse,ValidationDetail) - Response formatters (
formatSuccess,formatError) - Zod schemas (
successResponseSchema,errorResponseSchema) - JWT configuration types (
TokenTypeConfig,SignTokenOptions) - Error utilities (
createError,HTTP_STATUS_CODES) - No Node.js dependencies - browser compatible
// Server code
import { createFastify, generateId } from '@a_jackie_z/fastify'
import { formatSuccess } from '@a_jackie_z/fastify-types'
// Frontend code
import { successResponseSchema, type SuccessResponse } from '@a_jackie_z/fastify-types'Features
- Dynamic JWT Authentication - Flexible multi-token-type JWT system with per-type configuration
- Multiple Secret Keys - Support for key rotation with kid-based secret selection
- Per-Type Token Configuration - Configure header names, expiration, algorithm, issuer per token type
- Issuer Validation - Optional allowedIss validation for enhanced security
- Payload Schema Validation - Zod schema validation for token payloads
- Flexible Route Protection - Combine global required types with route-specific types
- Rate Limiting - Global rate limiting for API protection
- Swagger Documentation - Auto-generated API documentation with dynamic security schemes
- Zod Integration - Type-safe request/response validation
- Standardized Response Formatting - Consistent response shapes with
formatSuccessandformatErrorutilities - Automatic Error Handling - Field-level validation errors, auth errors, and rate limit errors with consistent formatting
- Custom Logger Support - Integrate any Fastify-compatible logger
- Health Check Plugin - Ready-to-use health check endpoint
- Plugin Helper - Utility for creating reusable Fastify plugins
Examples
1. Complete Setup with All Features
import { createFastify, runFastify } from '@a_jackie_z/fastify'
import { z } from 'zod'
const app = await createFastify({
// Optional: Integrate your logger
// logger: yourFastifyLogger,
// Rate limiting
rateLimit: {
global: {
max: 100,
timeWindow: '1 minute',
},
},
// Dynamic JWT authentication with multiple token types
jwt: {
// Multiple secrets for key rotation
secrets: {
v1: 'your-secret-key-v1',
v2: 'your-secret-key-v2',
},
defaultKeyId: 'v2', // Use v2 for signing new tokens
// Define token types with individual configurations
tokenTypes: {
access: {
headerName: 'authorization',
expiresIn: '15m',
algorithm: 'HS256',
iss: 'auth-service',
allowedIss: ['auth-service'],
payloadSchema: z.object({
identityId: z.string(),
role: z.string().optional(),
}),
},
refresh: {
headerName: 'authorization',
expiresIn: '7d',
algorithm: 'HS256',
},
service: {
headerName: 'x-service-authorization',
expiresIn: '1h',
algorithm: 'HS256',
iss: 'gateway',
allowedIss: ['gateway', 'internal-service'],
},
},
// Token types always checked (unless route has jwtTypes: false)
requiredTypes: ['service'],
debug: true, // Enable detailed error messages in development
},
// Swagger documentation
swagger: {
title: 'My API',
version: '1.0.0',
description: 'API documentation',
routePrefix: '/docs',
},
})
await runFastify(app, '0.0.0.0', 3000)JWT Authentication
Overview
The JWT system supports dynamic token types where each type can have its own:
- Header name (e.g.,
authorization,x-service-authorization) - Expiration time
- JWT algorithm, issuer (iss), and audience (aud)
- Allowed issuers for verification
- Zod schema for payload validation
Token Type Configuration
Each token type is configured independently:
// TokenTypeConfig is available in @a_jackie_z/fastify-types
import type { TokenTypeConfig } from '@a_jackie_z/fastify-types'
interface TokenTypeConfig {
headerName: string // Header to extract token from
expiresIn: string // Token expiration (e.g., '15m', '7d', '1h')
payloadSchema?: z.ZodSchema // Optional Zod schema for payload validation
algorithm?: Algorithm // JWT signing algorithm (default: 'HS256')
iss?: string // Token issuer
aud?: string // Token audience
allowedIss?: string[] // Allowed issuers for verification (bypass if undefined)
header?: { // Optional JWT header configuration
typ?: string // Token type (typically 'JWT')
cty?: string // Content type (e.g., 'application/json')
[key: string]: any // Custom header claims
}
}Multiple Secret Keys (Key Rotation)
Support multiple secrets for key rotation using kid-based selection:
jwt: {
secrets: {
v1: 'old-secret-key',
v2: 'new-secret-key',
v3: 'future-secret-key',
},
defaultKeyId: 'v2', // New tokens signed with v2
// Tokens with kid=v1, v2, or v3 can all be verified
}Route Protection
Routes can specify which token types they require:
// Public route - no authentication
app.get('/public', {
config: { jwtTypes: false }
}, handler)
// Protected by access token only
app.get('/user-data', {
config: { jwtTypes: ['access'] }
}, handler)
// Protected by both access and service tokens
app.get('/gateway-protected', {
config: { jwtTypes: ['access', 'service'] }
}, handler)
// No config - uses requiredTypes from global config
app.get('/auto-protected', handler)Global Required Types
The requiredTypes option specifies token types that are ALWAYS checked on all routes (unless explicitly bypassed with jwtTypes: false):
jwt: {
requiredTypes: ['service'], // Always verify service token
tokenTypes: { /* ... */ }
}
// Route A: No jwtTypes specified
// Verifies: ['service']
app.get('/api/data', handler)
// Route B: jwtTypes: ['access']
// Verifies: ['service', 'access'] (both combined!)
app.get('/api/user', { config: { jwtTypes: ['access'] } }, handler)
// Route C: jwtTypes: false
// Verifies: nothing (explicitly public)
app.get('/public', { config: { jwtTypes: false } }, handler)2. Authentication Flow - Login Route
import { formatSuccess, formatError } from '@a_jackie_z/fastify-types'
app.route({
method: 'POST',
url: '/auth/login',
config: {
jwtTypes: false, // Public route
},
schema: {
body: z.object({
username: z.string(),
password: z.string(),
}),
response: {
200: z.object({
status: z.number(),
success: z.boolean(),
data: z.object({
accessToken: z.string(),
refreshToken: z.string(),
}),
}),
401: z.object({
status: z.number(),
success: z.boolean(),
error: z.string(),
message: z.string(),
}),
},
},
handler: async (request, reply) => {
const { username, password } = request.body
// Validate credentials
if (username === 'admin' && password === 'secret') {
// Generate tokens using jwtService
const accessToken = app.jwtService.generateToken('access', {
identityId: 'user-123',
role: 'admin',
})
const refreshToken = app.jwtService.generateToken('refresh', {
identityId: 'user-123',
})
return reply.send(formatSuccess(200, { accessToken, refreshToken }))
}
return reply.status(401).send(
formatError(401, 'Unauthorized', 'Invalid credentials')
)
},
})3. Protected Route - Accessing JWT Payloads
app.route({
method: 'GET',
url: '/user/profile',
config: {
jwtTypes: ['access'], // Requires access token (+ any global requiredTypes)
},
schema: {
response: {
200: z.object({
status: z.number(),
success: z.boolean(),
data: z.object({
identityId: z.string(),
role: z.string(),
}),
}),
},
},
handler: async (request, reply) => {
// Access verified token payloads from Map
const accessPayload = request.jwtPayloads.get('access')
const servicePayload = request.jwtPayloads.get('service') // If in requiredTypes
return reply.send(formatSuccess(200, {
identityId: accessPayload.identityId,
role: accessPayload.role,
}))
},
})4. Service-to-Service Authentication
Configure service tokens for microservice communication:
jwt: {
tokenTypes: {
service: {
headerName: 'x-service-authorization',
expiresIn: '1h',
algorithm: 'HS256',
iss: 'gateway',
allowedIss: ['gateway', 'auth-service'],
payloadSchema: z.object({
serviceId: z.string(),
}),
},
},
requiredTypes: ['service'], // All routes require service token
}
// Gateway generates service token
const serviceToken = app.jwtService.generateToken('service', {
serviceId: 'gateway',
})
// Forward to backend service with service token
fetch('http://backend-service/api/data', {
headers: {
'Authorization': `Bearer ${userAccessToken}`,
'X-Service-Authorization': serviceToken, // No Bearer prefix for custom headers
},
})5. Token Refresh Flow
app.route({
method: 'POST',
url: '/auth/refresh',
config: {
jwtTypes: false, // Public endpoint
},
schema: {
body: z.object({
refreshToken: z.string(),
}),
},
handler: async (request, reply) => {
const { refreshToken } = request.body
try {
// Verify refresh token
const payload = app.jwtService.verifyToken('refresh', refreshToken)
// Generate new tokens
const newAccessToken = app.jwtService.generateToken('access', {
identityId: payload.identityId,
})
const newRefreshToken = app.jwtService.generateToken('refresh', {
identityId: payload.identityId,
})
return reply.send(formatSuccess(200, {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
}))
} catch (err) {
return reply.status(401).send(
formatError(401, 'Unauthorized', 'Invalid or expired refresh token')
)
}
},
})6. Custom Authorization Logic
Implement custom authorization in route handlers:
app.route({
method: 'DELETE',
url: '/admin/users/:id',
config: {
jwtTypes: ['access'],
},
handler: async (request, reply) => {
const accessPayload = request.jwtPayloads.get('access')
// Check role
if (accessPayload.role !== 'admin') {
return reply.status(403).send(
formatError(403, 'Forbidden', 'Admin role required')
)
}
// Check permissions
if (!accessPayload.permissions?.includes('delete')) {
return reply.status(403).send(
formatError(403, 'Forbidden', 'Delete permission required')
)
}
// Proceed with deletion
const { id } = request.params
await deleteUser(id)
return reply.send(formatSuccess(200, { message: 'User deleted' }))
},
})7. Issuer Validation
Enforce issuer validation for enhanced security:
jwt: {
tokenTypes: {
access: {
headerName: 'authorization',
expiresIn: '15m',
iss: 'auth-service', // Tokens generated with iss='auth-service'
allowedIss: ['auth-service'], // Only accept tokens from auth-service
},
service: {
headerName: 'x-service-authorization',
expiresIn: '1h',
iss: 'gateway',
allowedIss: ['gateway', 'legacy-gateway'], // Accept from multiple sources
},
},
}
// If allowedIss is undefined, issuer validation is bypassed8. Payload Schema Validation
Define Zod schemas for runtime payload validation:
import { z } from 'zod'
jwt: {
tokenTypes: {
access: {
headerName: 'authorization',
expiresIn: '15m',
payloadSchema: z.object({
identityId: z.string().uuid(),
role: z.enum(['user', 'admin', 'moderator']),
permissions: z.array(z.string()).optional(),
exp: z.number(), // JWT standard field
}),
},
},
}
// Tokens with invalid payloads will be rejected during verification9. JWT Header Configuration
Configure standard and custom JWT header claims per token type:
import { z } from 'zod'
jwt: {
tokenTypes: {
access: {
headerName: 'authorization',
expiresIn: '15m',
algorithm: 'HS256',
header: {
typ: 'JWT', // Standard: Token type
cty: 'application/json', // Standard: Content type
ver: '2.0', // Custom: API version
ctx: 'web', // Custom: Client context
},
},
service: {
headerName: 'x-service-authorization',
expiresIn: '1h',
algorithm: 'HS256',
header: {
typ: 'JWT',
env: 'production', // Custom: Environment
},
},
},
}
// Generated tokens will include these headers
// Verification will validate headers match the configurationStandard JWT Headers:
typ- Token type (typically 'JWT')cty- Content type for nested JWTs or specific contentalg- Algorithm (set viaalgorithmfield)kid- Key ID (automatically set fromdefaultKeyId)
Custom Headers:
- Any additional key-value pairs for application-specific needs
- Examples:
ver(version),ctx(context),env(environment),app(application ID)
Header Validation:
- During token verification, headers are validated against the configured values
- Tokens with mismatched headers will be rejected
- Provides additional security layer for token authenticity
10. Health Check Plugin
import { createFastify, runFastify, healthPlugin } from '@a_jackie_z/fastify'
const app = await createFastify()
// Register health check plugin
await app.register(healthPlugin)
// Health endpoint available at: GET /v1/health
// Response: { status: 200, message: 'ok' }
await runFastify(app, '0.0.0.0', 3000)10. Creating Custom Plugins
import { createFastify, runFastify, createFastifyPlugin } from '@a_jackie_z/fastify'
import { z } from 'zod'
// Create a reusable plugin
const myPlugin = createFastifyPlugin((app) => {
app.get('/plugin-route', {
schema: {
response: {
200: z.object({
message: z.string(),
}),
},
},
handler: async () => {
return { message: 'From plugin' }
},
})
})
// Use the plugin
const app = await createFastify()
await app.register(myPlugin)
await runFastify(app, '0.0.0.0', 3000)11. Zod Schema Validation
app.route({
method: 'POST',
url: '/users',
schema: {
body: z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().min(0).max(150).optional(),
}),
response: {
201: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
400: z.object({
error: z.string(),
}),
},
},
handler: async (request, reply) => {
const user = request.body
reply.status(201)
return {
id: 'generated-id',
...user,
}
},
})12. Response Formatting
All responses should follow a consistent format using the provided utility functions:
Success Response Format
import { formatSuccess } from '@a_jackie_z/fastify-types'
app.route({
method: 'GET',
url: '/users/:id',
schema: {
params: z.object({
id: z.string(),
}),
response: {
200: z.object({
status: z.number(),
success: z.boolean(),
data: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
}),
},
},
handler: async (request, reply) => {
const { id } = request.params
const user = await findUser(id) // Your logic here
return reply.status(200).send(
formatSuccess(200, user)
)
// Returns: { status: 200, success: true, data: { id, name, email } }
},
})Error Response Format
import { formatError } from '@a_jackie_z/fastify-types'
app.route({
method: 'DELETE',
url: '/users/:id',
handler: async (request, reply) => {
const { id } = request.params
const user = await findUser(id)
if (!user) {
return reply.status(404).send(
formatError(404, 'Not Found', `User with ID ${id} not found`)
)
// Returns: { status: 404, success: false, error: 'Not Found', message: '...' }
}
await deleteUser(id)
return reply.send(formatSuccess(200, { message: 'User deleted' }))
},
})Automatic Validation Error Formatting
Validation errors are automatically formatted with field-level details:
app.route({
method: 'POST',
url: '/users',
config: { jwtTypes: false },
schema: {
body: z.object({
username: z.string().min(3, 'Username must be at least 3 characters'),
email: z.string().email('Invalid email format'),
age: z.number().int().min(18, 'Must be at least 18 years old'),
}),
},
handler: async (request, reply) => {
// If validation fails, automatic error response:
// {
// status: 400,
// success: false,
// error: 'Validation Error',
// message: 'Request validation failed',
// details: [
// { field: 'username', message: 'Username must be at least 3 characters' },
// { field: 'email', message: 'Invalid email format' },
// { field: 'age', message: 'Must be at least 18 years old' }
// ]
// }
return reply.send(formatSuccess(200, request.body))
},
})Automatic Error Handling
All errors follow the standardized format automatically:
- Validation Errors (400): Field-level details with field name and message
- Authentication Errors (401): JWT token missing or invalid
- Authorization Errors (403): Insufficient permissions
- Not Found Errors (404): Resource not found
- Rate Limit Errors (429): Too many requests
- Server Errors (500): Internal server errors
All errors are automatically logged via fastify.log.error with request context.
// Authentication error (automatic)
// GET /protected without JWT token
// { status: 401, success: false, error: 'Unauthorized', message: 'Invalid or missing JWT token' }
// Authorization error (automatic)
// GET /admin without admin role
// { status: 403, success: false, error: 'Forbidden', message: 'Authorization failed' }
// Rate limit error (automatic)
// Too many requests
// { status: 429, success: false, error: 'Too Many Requests', message: 'Rate limit exceeded' }Configuration Options
CreateFastifyOptions
interface CreateFastifyOptions {
logger?: FastifyServerOptions['loggerInstance']
rateLimit?: {
global?: RateLimitPluginOptions
}
jwt?: {
secrets: Record<string, string> // Multiple secrets for key rotation
defaultKeyId: string // Default secret key ID for signing
tokenTypes: Record<string, TokenTypeConfig> // Token type configurations
requiredTypes?: string[] // Token types always checked (unless jwtTypes: false)
debug?: boolean // Enable detailed error messages
}
swagger?: {
title: string
version: string
description: string
routePrefix?: string // Default: '/docs/'
}
}
interface TokenTypeConfig {
headerName: string // Header to extract token from
expiresIn: string // Token expiration (e.g., '15m', '7d', '1h')
payloadSchema?: z.ZodSchema // Optional Zod schema for payload validation
algorithm?: Algorithm // JWT signing algorithm (default: 'HS256')
iss?: string // Token issuer
aud?: string // Token audience
allowedIss?: string[] // Allowed issuers for verification (bypass if undefined)
header?: { // Optional JWT header configuration
typ?: string // Token type (typically 'JWT')
cty?: string // Content type
[key: string]: any // Custom header claims
}
}Route JWT Configuration
Routes configure JWT requirements using jwtTypes:
// Public route - no JWT required
config: { jwtTypes: false }
// Protected route - requires specific token types
config: { jwtTypes: ['access'] }
// Multiple token types required
config: { jwtTypes: ['access', 'service'] }
// No config - uses global requiredTypes (if configured)
// No config specifiedAPI Reference
Server Functions (@a_jackie_z/fastify)
createFastify(options?: CreateFastifyOptions): Promise<FastifyServer>
Creates and configures a Fastify server instance with Zod support and optional plugins.
runFastify(fastify: FastifyServer, host: string, port: number): Promise<void>
Starts the Fastify server. Handles errors and exits the process if the server fails to start.
Parameters:
fastify- The Fastify server instancehost- The host to bind to (e.g., '0.0.0.0' or 'localhost')port- The port number to listen on
Example:
const app = await createFastify()
await runFastify(app, '0.0.0.0', 3000)Server-Side Crypto Utilities
import { generateId, generateSessionToken, generateSecureString } from '@a_jackie_z/fastify'
// Generate 16-character alphanumeric ID
const id = generateId() // "A1b2C3d4E5f6G7h8"
// Generate 64-character hex session token
const token = generateSessionToken() // "abc123...def789"
// Generate custom secure string
const customId = generateSecureString(8, '0123456789') // "42875391"Response Formatting Functions (@a_jackie_z/fastify-types)
formatSuccess<T>(status: number, data: T): SuccessResponse<T>
Creates a standardized success response.
Parameters:
status- HTTP status code (e.g., 200, 201)data- Response data of any type
Returns: SuccessResponse<T>
{
status: number
success: true
data: T
}Example:
return reply.send(formatSuccess(200, { id: '123', name: 'John' }))
// Returns: { status: 200, success: true, data: { id: '123', name: 'John' } }formatError(status: number, error: string, message: string, details?: ValidationDetail[]): ErrorResponse
Creates a standardized error response.
Parameters:
status- HTTP status code (e.g., 400, 404, 500)error- Error type/name (e.g., 'Not Found', 'Validation Error')message- Human-readable error messagedetails- Optional array of validation error details
Returns: ErrorResponse
{
status: number
success: false
error: string
message: string
details?: ValidationDetail[]
}Example:
return reply.status(404).send(
formatError(404, 'Not Found', 'User not found')
)
// Returns: { status: 404, success: false, error: 'Not Found', message: 'User not found' }Response Type Interfaces (@a_jackie_z/fastify-types)
SuccessResponse<T>
interface SuccessResponse<T> {
status: number
success: true
data: T
}ErrorResponse
interface ErrorResponse {
status: number
success: false
error: string
message: string
details?: ValidationDetail[]
}ValidationDetail
interface ValidationDetail {
field: string
message: string
}Zod Schema Helpers (@a_jackie_z/fastify-types)
For defining response schemas with Zod validation:
successResponseSchema<T>(dataSchema: T): ZodObject
Creates a Zod schema for standardized success responses.
Parameters:
dataSchema- Zod schema for the data payload
Returns: Zod object schema matching SuccessResponse<T>
Example:
import { z } from 'zod'
import { successResponseSchema } from '@a_jackie_z/fastify-types'
const userSchema = z.object({
id: z.string(),
name: z.string(),
})
const loginResponseSchema = {
body: z.object({
username: z.string(),
password: z.string(),
}),
response: {
200: successResponseSchema(z.object({
accessToken: z.string(),
refreshToken: z.string(),
})),
},
}errorResponseSchema: ZodObject
Zod schema for standardized error responses. Matches ErrorResponse interface.
Example:
import { errorResponseSchema } from '@a_jackie_z/fastify-types'
const myRouteSchema = {
response: {
200: successResponseSchema(z.object({ success: z.boolean() })),
400: errorResponseSchema,
401: errorResponseSchema,
404: errorResponseSchema,
},
}JWT Service API
FastifyJwtService
The FastifyJwtService is automatically decorated on your Fastify instance when JWT is configured. Access it via app.jwtService.
Methods
generateToken(typeName: string, payload: Record<string, any>): string
Generate a JWT token for a specific token type.
const accessToken = app.jwtService.generateToken('access', {
identityId: 'user-123',
role: 'admin',
})
const serviceToken = app.jwtService.generateToken('service', {
serviceId: 'gateway',
})verifyToken(typeName: string, token: string): any
Verify and decode a JWT token for a specific token type. Returns the payload if valid, throws error if invalid.
try {
const payload = app.jwtService.verifyToken('access', token)
console.log(payload.identityId)
} catch (err) {
console.error('Invalid token:', err.message)
}extractTokenFromHeader(headerValue: string | string[] | undefined, typeName: string): string | null
Extract a token from a header value for a specific token type.
const token = app.jwtService.extractTokenFromHeader(
request.headers.authorization,
'access'
)getTokenTypeConfig(typeName: string): TokenTypeConfig | undefined
Get the configuration for a specific token type.
const config = app.jwtService.getTokenTypeConfig('access')
console.log(config.expiresIn) // '15m'getTokenTypeNames(): string[]
Get all configured token type names.
const types = app.jwtService.getTokenTypeNames()
console.log(types) // ['access', 'refresh', 'service']Testing Your API
- Start your server
- Access Swagger documentation at
http://localhost:3000/docs - Test authentication:
# Login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}'
# Use access token
curl http://localhost:3000/user/profile \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Service-to-service (with both tokens)
curl http://localhost:3000/api/data \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-Service-Authorization: YOUR_SERVICE_TOKEN"License
MIT
