Server Guide
TSDIAPI's server component provides a powerful and flexible foundation for building APIs. This guide covers the core server features and how to use them effectively.
Server Features
- Fastify-based HTTP server
- TypeScript-first development
- Dependency injection with TypeDI
- TypeBox schema validation
- Plugin system
- Swagger/OpenAPI integration
Basic Setup
The createApp
function is the entry point for creating a TSDIAPI server. It provides a powerful and flexible way to configure your application:
import { createApp } from '@tsdiapi/server';
import { Type } from '@sinclair/typebox';
import { PrismaClient } from "@generated/prisma/client.js";
import PrismaPlugin from "@tsdiapi/prisma";
// Define configuration schema
const ConfigSchema = Type.Object({
PORT: Type.Number({ default: 3000 }),
HOST: Type.String({ default: 'localhost' }),
DATABASE_URL: Type.String()
});
type Config = Static<typeof ConfigSchema>;
// main.ts
// Create and start the application
const app = await createApp<Config>({
// Basic configuration
apiDir: './api', // Directory containing API controllers
configSchema: ConfigSchema, // TypeBox schema for environment variables
logger: true, // Enable Fastify logger
// Fastify configuration
fastifyOptions: (defaults) => ({
...defaults,
trustProxy: true
}),
// Security configuration
corsOptions: {
origin: ['https://myapp.com'],
credentials: true
},
helmetOptions: {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"]
}
}
},
// Documentation configuration
swaggerOptions: {
openapi: {
info: {
title: 'My API',
version: '1.0.0'
}
}
},
// Plugins
plugins: [PrismaPlugin({ client: PrismaClient })],
// Lifecycle hooks
onInit: async (ctx) => {
console.log('Initializing application...');
},
afterStart: async (ctx) => {
console.log(`Server started on ${ctx.fastify.server.address()}`);
}
});
console.log(`Server running at http://${app.config.HOST}:${app.config.PORT}`);
Configuration Options
The createApp
function accepts the following configuration options:
-
Basic Configuration:
apiDir
: Directory containing your API controllers and servicesconfigSchema
: TypeBox schema for validating configurationlogger
: Enable/disable Fastify loggerfileLoader
: Custom file loader for handling uploadsplugins
: Array of plugins to use
-
Fastify Configuration:
fastifyOptions
: Customize Fastify server optionscorsOptions
: Configure CORShelmetOptions
: Configure security headersswaggerOptions
: Configure OpenAPI documentationswaggerUiOptions
: Configure Swagger UIstaticOptions
: Configure static file servingmultipartOptions
: Configure multipart form handling
-
Lifecycle Hooks:
onInit
: Called when the app is initializedbeforeStart
: Called before the server startspreReady
: Called before the server is readyafterStart
: Called after the server starts
AppContext
The AppContext
object is available in lifecycle hooks and plugins, providing access to:
interface AppContext<T> {
fastify: FastifyInstance; // Fastify server instance
environment: 'production' | 'development';
appDir: string; // Application directory
options: AppOptions<T>; // Application options
fileLoader?: FileLoader; // File loader function
projectConfig: AppConfig<T>; // Project configuration
projectPackage: Record<string, any>; // Package.json contents
plugins?: Record<string, AppPlugin>; // Loaded plugins
useRoute: RouteBuilder; // Route builder function
}
Server Configuration
The server configuration is defined using TypeBox schemas for environment variables. This schema serves several important purposes:
- Environment Variable Validation: Validates that all required environment variables are present and have correct types
- Type Safety: Provides full TypeScript type checking for configuration
- Documentation: Documents required environment variables
- Transformation: Automatically transforms environment variables to correct types
Basic Configuration Schema
import { Type, Static } from '@sinclair/typebox';
const ConfigSchema = Type.Object({
// Required variables
PORT: Type.Number({ default: 3000 }),
HOST: Type.String({ default: 'localhost' }),
DATABASE_URL: Type.String()
});
type Config = Static<typeof ConfigSchema>;
Environment Variable Transformation
The schema automatically handles type transformations:
PORT=3000 // Will be transformed to number
ENABLE_CACHE=true // Will be transformed to boolean
Using the Configuration
The configuration is automatically loaded and validated when the application starts. You can access it through the AppContext
:
createApp<Config>({
configSchema: ConfigSchema,
onInit: async (ctx) => {
// Access configuration
const port = ctx.projectConfig.get('PORT', 3000);
const host = ctx.projectConfig.get('HOST', 'localhost');
console.log(`Server will run on ${host}:${port}`);
}
});
Common Environment Variables
PORT=3000 # Server port
HOST=localhost # Server host
APP_NAME=MyApp # Application name
APP_VERSION=1.0.0 # Application version
DATABASE_URL=... # Database connection string
Feature Modules
Create feature modules to organize your API:
import { AppContext } from "@tsdiapi/server";
import { Type } from "@sinclair/typebox";
const UserSchema = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String()
});
export default function UsersModule({ useRoute }: AppContext): void {
useRoute("users")
.get("/:id")
.summary("Get user by ID")
.description("Retrieves user information by ID")
.tags(["Users"])
.params(Type.Object({ id: Type.String() }))
.code(200, UserSchema)
.code(404, Type.Object({ error: Type.String() }))
.handler(async (req) => {
// Implementation
})
.build();
}
Services and Dependency Injection
Use TypeDI for dependency injection:
import { Service } from 'typedi';
@Service()
class UserService {
async findById(id: string) {
// Implementation
}
}
// Use in route handler
useRoute()
.get('/users/:id')
.handler(async (req) => {
const userService = Container.get(UserService);
return userService.findById(req.params.id);
});
Error Handling
TSDIAPI provides multiple approaches for error handling:
1. Standard Error Handling
The basic approach using standard HTTP status codes:
useRoute("contacts")
.get("/")
.version('1')
.code(200, Type.Object({ data: Type.Array(ContactSchema) }))
.code(400, Type.Object({ error: Type.String() }))
.code(404, Type.Object({ error: Type.String() }))
.handler(async (req) => {
// Implementation
});
2. Response Codes Builder
A more convenient way to register multiple response codes at once:
import { buildResponseCodes } from "@tsdiapi/server";
useRoute("contacts")
.get("/")
.version('1')
.codes(buildResponseCodes(Type.Array(ContactSchema)))
.handler(async (req) => {
// Implementation
});
This automatically registers standard response codes (200, 400, 401, 403, 404, 409, 422, 429, 500, 503) with the provided schema.
3. Typed Error Responses
For more type-safe error handling, you can use the provided error response classes:
import {
ResponseBadRequest,
ResponseNotFound,
ResponseUnauthorized,
// ... other error types
} from "@tsdiapi/server";
const handler = async (req: FastifyRequest) => {
try {
// Your implementation
} catch (error) {
throw new ResponseBadRequest("Invalid contact data");
}
}
useRoute("contacts")
.post("/")
.version('1')
.codes(buildResponseCodes(ContactSchema))
.handler(handler);
4. Custom Error Schemas
You can define custom error schemas for more detailed error responses:
import { useResponseErrorSchema } from "@tsdiapi/server";
const ValidationErrorSchema = Type.Object({
field: Type.String(),
message: Type.String()
});
const { register: errorRegister, send: sendError } = useResponseErrorSchema(400, ValidationErrorSchema);
const { register: successRegister, send: sendSuccess } = useResponseSchema(200, ContactSchema);
useRoute("contacts")
.post("/")
.version('1')
.code(...successRegister)
.code(...errorRegister)
.handler(async (req) => {
try {
sendSuccess(req.body);
} catch (error) {
throw sendError("Validation failed", {
field: "email",
message: "Invalid email format"
});
}
});
5. Combined Response Schemas
For a more integrated approach, you can use useResponseSchemas
to handle both success and error responses:
import { useResponseSchemas } from "@tsdiapi/server";
const ContactSchema = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String()
});
const ValidationErrorSchema = Type.Object({
field: Type.String(),
message: Type.String()
});
const { codes, sendError, sendSuccess, send } = useResponseSchemas(
ContactSchema,
ValidationErrorSchema
);
useRoute("contacts")
.post("/")
.version('1')
.codes(codes)
.handler(async (req) => {
try {
// Success case
const contact = await createContact(req.body);
return sendSuccess(contact);
// Or using the combined send function
// return send(contact);
} catch (error) {
// Error case
return sendError("Validation failed", {
field: "email",
message: "Invalid email format"
});
// Or using the combined send function
// return send({ error: "Validation failed", details: { field: "email", message: "Invalid email format" } });
}
});
The useResponseSchemas
function provides:
codes
: Pre-configured response codes for both success and error casessendError
: Function to send error responsessendSuccess
: Function to send success responsessend
: Combined function that can handle both success and error responses
6. Response Helpers
For quick response creation, use the provided helper functions:
import {
response200,
response400,
// ... other helpers
} from "@tsdiapi/server";
useRoute("contacts")
.get("/:id")
.version('1')
.codes(buildResponseCodes(ContactSchema))
.handler(async (req) => {
const contact = await findContact(req.params.id);
if (!contact) {
return response400("Contact not found");
}
return response200(contact);
});
Best Practices
- Use appropriate HTTP status codes for different error types
- Include detailed error information in responses
- Use TypeBox schemas for error response validation
- Implement proper error logging
- Handle both synchronous and asynchronous errors
- Use typed error responses for better type safety
- Consider using the response codes builder for standard endpoints
- Use custom error schemas when you need detailed error information
Best Practices
- Use TypeBox schemas for configuration and validation
- Organize code into feature modules
- Use dependency injection for services
- Implement proper error handling
- Document your API with OpenAPI/Swagger