@tsdiapi/server
Version:
A fully ESM-based, modular TypeScript server built on Fastify
582 lines (471 loc) • 17 kB
Markdown
# 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 /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 '/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 '/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 (`/cli`) provides automated project management and configuration:
1. **Project Creation**:
```bash
npx /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 /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 /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 `/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