UNPKG

@tsdiapi/server

Version:

A fully ESM-based, modular TypeScript server built on Fastify

582 lines (471 loc) 17 kB
# TSDIAPI Server Configuration Guide This guide explains how to configure and customize your TSDIAPI server using the `createApp` function. ## 🚀 Basic Setup When you create a new project using the CLI (`npx @tsdiapi/cli create app`), a basic configuration is automatically generated in `src/main.ts`: ```typescript import { PrismaClient } from "@generated/prisma/client.js"; import { createApp } from "@tsdiapi/server"; import { ConfigType, ConfigSchema } from "./app.config.js"; import PrismaPlugin from "@tsdiapi/prisma"; createApp<ConfigType>({ configSchema: ConfigSchema, plugins: [PrismaPlugin({ client: PrismaClient })] }); ``` ## ⚙️ Configuration Options The `createApp` function accepts an object with the following configuration options: ### Basic Configuration ```typescript createApp<ConfigType>({ // Directory containing your API controllers and services apiDir?: string; // TypeBox schema for validating your configuration configSchema?: TObject; // Enable/disable Fastify logger logger?: boolean; // Custom file loader for handling uploads fileLoader?: FileLoader; // Array of plugins to use plugins?: AppPlugin[]; }) ``` ### Fastify Configuration ```typescript createApp<ConfigType>({ // Customize Fastify server options fastifyOptions?: (defaultOptions: FastifyServerOptions) => FastifyServerOptions; // Configure CORS corsOptions?: FastifyCorsOptions | boolean | (defaultOptions: FastifyCorsOptions) => FastifyCorsOptions; // Configure Helmet security headers helmetOptions?: FastifyHelmetOptions | boolean | (defaultOptions: FastifyHelmetOptions) => FastifyHelmetOptions; // Configure Swagger documentation swaggerOptions?: FastifyDynamicSwaggerOptions | (defaultOptions: FastifyDynamicSwaggerOptions) => FastifyDynamicSwaggerOptions; // Configure Swagger UI swaggerUiOptions?: FastifySwaggerUiOptions | (defaultOptions: FastifySwaggerUiOptions) => FastifySwaggerUiOptions; // Configure static file serving staticOptions?: FastifyStaticOptions | boolean | (defaultOptions: FastifyStaticOptions) => FastifyStaticOptions; // Configure multipart form handling multipartOptions?: FastifyMultipartAttachFieldsToBodyOptions | (defaultOptions: FastifyMultipartAttachFieldsToBodyOptions) => FastifyMultipartAttachFieldsToBodyOptions; }) ``` ### Lifecycle Hooks ```typescript createApp<ConfigType>({ // Called when the app is initialized onInit?: (ctx: AppContext<ConfigType>) => Promise<void> | void; // Called before the server starts beforeStart?: (ctx: AppContext<ConfigType>) => Promise<void> | void; // Called before the server is ready preReady?: (ctx: AppContext<ConfigType>) => Promise<void> | void; // Called after the server starts afterStart?: (ctx: AppContext<ConfigType>) => Promise<void> | void; }) ``` ## 📦 Plugin System Plugins can extend the functionality of your TSDIAPI server. Here's how to create and use them: ```typescript // Example plugin definition const MyPlugin: AppPlugin = { name: 'my-plugin', services: [MyService], // Optional: Array of service classes config: { /* plugin configuration */ }, // Lifecycle hooks onInit: async (ctx) => { // Initialize plugin }, beforeStart: async (ctx) => { // Run before server starts }, afterStart: async (ctx) => { // Run after server starts }, preReady: async (ctx) => { // Run before server is ready } }; // Using the plugin createApp<ConfigType>({ plugins: [MyPlugin] }); ``` ## 🔧 Environment Configuration TSDIAPI automatically loads environment variables from `.env` files. The following variables are supported: ```env PORT=3000 # Server port HOST=localhost # Server host APP_NAME=MyApp # Application name APP_VERSION=1.0.0 # Application version ``` ## 📝 Type Safety The `createApp` function is fully type-safe. When you create a new project using the CLI, it automatically generates a configuration schema file (`app.config.ts`) that defines the structure of your environment variables: ```typescript // app.config.ts import { Type, Static } from '@sinclair/typebox'; export const ConfigSchema = Type.Object({ NAME: Type.String(), VERSION: Type.String(), PORT: Type.Number(), HOST: Type.String(), DATABASE_URL: Type.String(), DATABASE_TYPE: Type.String() }); export type ConfigType = Static<typeof ConfigSchema>; ``` This schema serves several important purposes: 1. **Environment Variable Validation**: The schema validates that all required environment variables are present and have the correct types. 2. **Type Safety**: The `ConfigType` type is automatically generated from the schema, providing full TypeScript type checking. 3. **Documentation**: The schema serves as documentation for the required environment variables. 4. **Transformation**: Environment variables are automatically transformed to their correct types (e.g., strings to numbers). ### Using Custom Environment Variables You can extend the default schema with your own environment variables: ```typescript // app.config.ts import { Type, Static } from '@sinclair/typebox'; export const ConfigSchema = Type.Object({ // Default variables NAME: Type.String(), VERSION: Type.String(), PORT: Type.Number(), HOST: Type.String(), DATABASE_URL: Type.String(), DATABASE_TYPE: Type.String(), // Custom variables JWT_SECRET: Type.String(), API_KEY: Type.String(), ENABLE_CACHE: Type.Boolean(), CACHE_TTL: Type.Number(), ALLOWED_ORIGINS: Type.Array(Type.String()) }); export type ConfigType = Static<typeof ConfigSchema>; ``` ### Environment Variable Transformation The schema automatically handles type transformations: ```typescript // .env PORT=3000 // Will be transformed to number ENABLE_CACHE=true // Will be transformed to boolean ``` ### Required vs Optional Variables You can mark variables as optional using `Type.Optional()`: ```typescript export const ConfigSchema = Type.Object({ // Required variables DATABASE_URL: Type.String(), // Optional variables CACHE_TTL: Type.Optional(Type.Number()), ENABLE_LOGGING: Type.Optional(Type.Boolean()) }); ``` ### Using the Configuration The configuration is automatically loaded and validated when the application starts. You can access it through the `AppContext`: ```typescript createApp<ConfigType>({ onInit: async (ctx) => { // Access configuration const port = ctx.projectConfig.get('PORT', 3000); const host = ctx.projectConfig.get('HOST', 'localhost'); const databaseUrl = ctx.projectConfig.get('DATABASE_URL'); console.log(`Server will run on ${host}:${port}`); } }); ``` ## 🎯 Example Configuration Here's a complete example of a configured TSDIAPI server: ```typescript import { PrismaClient } from "@generated/prisma/client.js"; import { createApp } from "@tsdiapi/server"; import { ConfigType, ConfigSchema } from "./app.config.js"; import PrismaPlugin from "@tsdiapi/prisma"; import AuthPlugin from "./plugins/auth.js"; createApp<ConfigType>({ apiDir: './api', configSchema: ConfigSchema, logger: true, // 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' } } }, // File upload configuration fileLoader: async (file) => { // Custom file processing return file; }, // Plugins plugins: [ PrismaPlugin({ client: PrismaClient }), AuthPlugin() ], // Lifecycle hooks onInit: async (ctx) => { console.log('Initializing application...'); }, afterStart: async (ctx) => { console.log(`Server started on ${ctx.fastify.server.address()}`); } }); ``` ## 🔍 AppContext The `AppContext` object is available in lifecycle hooks and plugins. It provides access to: ```typescript 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 } ``` ## 📚 Related Documentation - [Routing Guide](./readme.routing.md) - [Prisma Integration](./readme.prisma.md) ### Automatic Configuration Expansion The TSDIAPI CLI (`@tsdiapi/cli`) provides automated project management and configuration: 1. **Project Creation**: ```bash npx @tsdiapi/cli create myapi ``` This automatically: - Sets up the project structure - Creates initial configuration files - Generates `app.config.ts` with basic environment variables 2. **Plugin Integration**: ```bash npx @tsdiapi/cli plugins add prisma ``` When you add a plugin, the CLI: - Adds necessary dependencies - Updates project configuration - Sets up plugin-specific environment variables 3. **Code Generation**: ```bash npx @tsdiapi/cli generate module user ``` The CLI automatically: - Creates module structure - Sets up basic configuration - Generates necessary files ### Configuration Management The CLI automatically manages your project configuration through: 1. **Environment Variables**: ```env PORT=3000 HOST=localhost ``` These are automatically loaded and validated against your `ConfigSchema`. 2. **Plugin Configuration**: When you add a plugin, the CLI automatically: - Updates `.env` with required variables - Extends `ConfigSchema` with plugin-specific types - Sets up default configuration values 3. **Project Structure**: The CLI maintains a consistent project structure This automation ensures that your project configuration stays consistent and type-safe as you add new features and plugins. ## 📁 File Handling TSDIAPI provides robust file handling capabilities through the `@fastify/multipart` plugin. Here's how it works: ### Basic File Upload ```typescript export default function uploadController({ useRoute }: AppContext) { useRoute() .post("/upload") .acceptMultipart() .body(Type.Object({ file: Type.String({ format: "binary" }), metadata: Type.Object({ title: Type.String() }) })) .fileOptions({ maxFileSize: 1024 * 1024 * 5, // 5MB accept: ["image/jpeg", "image/png"] }, "file") .handler(async (req) => { // Files are available in req.tempFiles const file = req.tempFiles[0]; return { status: 200, data: { url: file.url } }; }) .build(); } ``` ### File Storage and Processing 1. **Temporary Storage**: - Files are stored in memory as `Buffer` objects - Accessible through `req.tempFiles` array in route handlers - Each file has a unique ID and metadata 2. **File Object Structure**: ```typescript interface UploadFile { id: string; // Unique identifier fieldname: string; // Form field name filename: string; // Original filename encoding: string; // File encoding mimetype: string; // MIME type filesize: number; // File size in bytes buffer: Buffer; // File content url?: string; // URL after processing meta?: Record<string, any>; // Custom metadata s3bucket?: string; // S3 bucket if uploaded s3region?: string; // S3 region if uploaded } ``` 3. **File Processing Options**: a. **Global File Loader** (Recommended): ```typescript createApp<ConfigType>({ fileLoader: async (file: UploadFile) => { // Example: Upload to S3 const s3Url = await uploadToS3(file.buffer, { bucket: 'my-bucket', key: `${file.id}-${file.filename}` }); return { ...file, url: s3Url, s3bucket: 'my-bucket', s3region: 'us-east-1' }; } }); ``` b. **Manual Processing in Handler**: ```typescript useRoute() .post("/upload") .acceptMultipart() .handler(async (req) => { const file = req.tempFiles[0]; // Manual upload const s3Url = await uploadToS3(file.buffer, { bucket: 'my-bucket', key: `${file.id}-${file.filename}` }); return { status: 200, data: { url: s3Url, filename: file.filename } }; }) .build(); ``` 4. **File Access in Routes**: ```typescript useRoute() .post("/process-files") .acceptMultipart() .handler(async (req) => { // Access all uploaded files const files = req.tempFiles; // Process each file const results = await Promise.all( files.map(async (file) => { // File is already processed if global fileLoader is set if (file.url) { return { url: file.url }; } // Manual processing if needed const url = await uploadToStorage(file.buffer); return { url }; }) ); return { status: 200, data: { files: results } }; }) .build(); ``` ### Best Practices 1. **Use Global File Loader**: - Set up `fileLoader` in `createApp` for consistent file processing - Handles all file uploads automatically - Can be configured for different storage providers 2. **File Validation**: ```typescript .fileOptions({ maxFileSize: 5 * 1024 * 1024, // 5MB accept: ["image/*"], // All image types maxFiles: 5 // Maximum files per request }) ``` 3. **Error Handling**: ```typescript useRoute() .post("/upload") .acceptMultipart() .code(400, Type.Object({ error: Type.String() })) .handler(async (req) => { if (!req.tempFiles?.length) { return { status: 400, data: { error: "No files uploaded" } }; } // Process files... }) .build(); ``` ### File Object Structure ```typescript interface UploadFile { id: string; // Unique identifier fieldname: string; // Form field name filename: string; // Original filename encoding: string; // File encoding mimetype: string; // MIME type filesize: number; // File size in bytes buffer: Buffer; // File content url?: string; // URL after processing meta?: Record<string, any>; // Custom metadata s3bucket?: string; // S3 bucket if uploaded s3region?: string; // S3 region if uploaded } ``` ### Error Handling ```typescript useRoute() .post("/upload") .acceptMultipart() .fileOptions({ maxFileSize: 5 * 1024 * 1024, accept: ["image/jpeg", "image/png"] }) .code(400, Type.Object({ errors: Type.Array(Type.String()) })) .handler(async (req) => { // File validation errors are automatically handled return { status: 200, data: { success: true } }; }) .build(); ``` ### Security Considerations 1. **File Size Limits**: Both global and per-route limits prevent DoS attacks 2. **MIME Type Validation**: Ensures only allowed file types are uploaded 3. **Temporary Storage**: Files are stored in memory and processed immediately 4. **Unique Identifiers**: Each file gets a unique ID to prevent conflicts