### Basic chista-express Setup with Service and Middleware (TypeScript) Source: https://github.com/koorchik/node-chista-express/blob/master/README.md A quick start example showing how to define a service, configure the ExpressRestApiBuilder with a service factory and session loading, build the API, and start the Express server. This illustrates the core workflow of setting up routes and handling dependencies. ```typescript import { ExpressRestApiBuilder, RestApiError } from 'chista-express'; // Assume Database and myDatabase are defined elsewhere declare class Database { query(sql: string): Promise; } declare const myDatabase: Database; // Define your service class UsersList { constructor(private deps: { db: Database, session: any }) {} async run() { console.log('Session:', this.deps.session); return this.deps.db.query('SELECT * FROM users'); } } // Create builder const builder = new ExpressRestApiBuilder({ logger: console, // Service factory - inject your dependencies here createService: (Service, { session }) => { return new Service({ session, db: myDatabase }); }, // Required for authenticated routes (services) loadSession: async (req) => { const token = req.headers['x-access-token']; // In a real app, you'd validate the token and fetch user data return { userId: 1, token }; }, services: [ ['GET', '/users', UsersList] ] }); // Get Express app and add custom middleware const app = builder.getApp(); // Finalize routes builder.build(); // Start server app.listen(3000, () => { console.log('Server running on port 3000'); }); ``` -------------------------------- ### Typed Service Example in TypeScript Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Provides an example of a typed service implementation using TypeScript. It demonstrates defining input and output types for a service and implementing the 'run' method with dependency injection. ```typescript import type { Service } from 'chista-express'; interface UserInput { name: string; email: string; } interface User { id: number; name: string; email: string; } class UsersCreate implements Service { constructor(private deps: { db: Database; session: Session }) {} async run(input: UserInput): Promise { return this.deps.db.users.create({ ...input, createdBy: this.deps.session.userId, }); } } ``` -------------------------------- ### Install chista-express Package Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Command to install the chista-express library using npm. This is the first step to integrating the REST API builder into your Node.js project. ```bash npm install chista-express ``` -------------------------------- ### Basic API Testing Setup with Supertest Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Provides a basic setup for testing API endpoints using supertest and chista-express. It includes setting up the ExpressRestApiBuilder, mocking dependencies, and writing tests for listing and creating users. Requires 'supertest' and 'chista-express'. ```typescript import request from 'supertest'; import { ExpressRestApiBuilder, RestApiError } from 'chista-express'; describe('User API', () => { let builder: ExpressRestApiBuilder; const mockDb = new Map(); beforeEach(() => { mockDb.clear(); builder = new ExpressRestApiBuilder({ loadSession: async () => ({ userId: 1 }), createService: (Service) => new Service({ db: mockDb }), services: [ ['GET', '/users', UsersList], ['POST', '/users', UsersCreate], ], }); builder.build(); }); test('should list users', async () => { const response = await request(builder.getApp()) .get('/api/users') .expect(200); expect(response.body.success).toBe(true); expect(response.body.result).toBeInstanceOf(Array); }); test('should create user', async () => { const response = await request(builder.getApp()) .post('/api/users') .send({ name: 'John', email: 'john@example.com' }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.result.name).toBe('John'); }); }); ``` -------------------------------- ### Testing API Endpoints Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Provides examples for setting up tests using supertest, including basic tests, error response testing, and authentication testing. ```APIDOC ## Testing This library is designed to be easily testable with supertest. ### Basic Test Setup ```typescript import request from 'supertest'; import { ExpressRestApiBuilder, RestApiError } from 'chista-express'; describe('User API', () => { let builder: ExpressRestApiBuilder; const mockDb = new Map(); beforeEach(() => { mockDb.clear(); builder = new ExpressRestApiBuilder({ loadSession: async () => ({ userId: 1 }), createService: (Service) => new Service({ db: mockDb }), services: [ ['GET', '/users', UsersList], ['POST', '/users', UsersCreate], ], }); builder.build(); }); test('should list users', async () => { const response = await request(builder.getApp()) .get('/api/users') .expect(200); expect(response.body.success).toBe(true); expect(response.body.result).toBeInstanceOf(Array); }); test('should create user', async () => { const response = await request(builder.getApp()) .post('/api/users') .send({ name: 'John', email: 'john@example.com' }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.result.name).toBe('John'); }); }); ``` ### Testing Error Responses ```typescript test('should return validation error', async () => { const response = await request(builder.getApp()) .post('/api/users') .send({ email: 'invalid' }) // missing required name .expect(422); expect(response.body.success).toBe(false); expect(response.body.error.fields).toBeDefined(); }); test('should handle authentication errors', async () => { const authBuilder = new ExpressRestApiBuilder({ loadSession: async () => { throw new RestApiError({ message: 'Unauthorized' }, 401); }, createService: (Service) => new Service(), services: [['GET', '/protected', ProtectedService]], }); authBuilder.build(); await request(authBuilder.getApp()) .get('/api/protected') .expect(401); }); ``` ### Testing with Authentication Headers ```typescript test('should accept token in header', async () => { const response = await request(builder.getApp()) .get('/api/users') .set('x-access-token', 'valid-token') .expect(200); expect(response.body.success).toBe(true); }); ``` ``` -------------------------------- ### Service Execution with createService in TypeScript Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Demonstrates using `createService` for dependency injection, allowing customization of service instantiation and error mapping. This is the recommended approach for simpler setups. ```typescript const builder = new ExpressRestApiBuilder({ logger, // Factory function to instantiate services with dependencies createService: (Service, { session }) => { return new Service({ session, db: database, mappers: dataMappers, userId: session?.userId }); }, // Optional: Transform domain errors to RestApiError mapError: (error) => { if (error instanceof MyDomainError) { return new RestApiError(error.toObject(), 200); } return undefined; // Use default 500 handling }, // Optional: Customize input extraction (has sensible defaults) extractInput: (context) => ({ ...context.request.query, ...context.request.params, ...context.request.body }), services: [...] }); ``` -------------------------------- ### Adding Middleware with getApp() and build() (JavaScript) Source: https://context7.com/koorchik/node-chista-express/llms.txt Demonstrates how to obtain the Express application instance using `getApp()` to add custom middleware before and after route registration. Middleware added before `build()` executes before all service routes, while middleware added after `build()` runs only if no service route matches. This example includes CORS, Helmet, compression, static file serving, and a 404 handler. ```javascript import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; const builder = new ExpressRestApiBuilder({ /* config */ }); const app = builder.getApp(); // Middleware added BEFORE build() runs BEFORE all service routes app.use(cors({ origin: '*' })); app.use(helmet()); app.use(compression()); // Register all routes builder.build(); // Routes added AFTER build() run only if no service matched app.use('/static', express.static('public')); app.use('*', (req, res) => res.status(404).json({ error: 'Not found' })); app.listen(3000); ``` -------------------------------- ### Define Service with Chista and Validation Source: https://github.com/koorchik/node-chista-express/blob/master/README.md An example of creating a service using the 'chista' package, including defining validation rules with LIVR and implementing the execute method. This demonstrates automatic validation before the execute method is called. Requires 'chista' package. ```typescript import { ServiceBase, ServiceError } from 'chista'; const validation = { name: ['required', { min_length: 1 }], email: ['required', 'email'], } as const; class UsersCreate extends ServiceBase { static validation = validation; async execute(data: { name: string; email: string }) { // Validation runs automatically before execute() return this.db.users.create(data); } } ``` -------------------------------- ### RunService Type Definition and Example Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Defines the RunService type, providing full control over service execution. It accepts a ServiceClass and RequestContext, extracts input, instantiates the service, and runs its 'run' method. ```typescript type RunService = (Service: ServiceClass, context: RequestContext) => Promise; // Example usage const runService: RunService = async (Service, context) => { const { request, session } = context; const input = { ...request.query, ...request.params, ...request.body }; const service = new Service({ session, db: database }); return await service.run(input); }; ``` -------------------------------- ### Service Execution Configuration: createService vs runService (TypeScript) Source: https://context7.com/koorchik/node-chista-express/llms.txt Compares two configuration options for ExpressRestApiBuilder: `createService` for dependency injection and automatic input extraction, and `runService` for manual control over service instantiation, input extraction, and execution. It provides code examples for both approaches. ```typescript // Option 1: createService (Recommended) // Library handles input extraction and service.run() call const builder1 = new ExpressRestApiBuilder({ createService: (Service, context) => { return new Service({ session: context.session, db: database, logger: console, }); }, // Optional: customize input extraction extractInput: (context) => ({ ...context.request.query, ...context.request.params, ...context.request.body, clientIp: context.request.ip, }), // Optional: transform domain errors mapError: (error) => { if (error instanceof ValidationError) { return new RestApiError(error.toObject(), 400); } return undefined; }, loadSession: async (req) => ({ userId: 1 }), services: [['GET', '/users', UsersList]], }); // Option 2: runService (Full Control) // You handle everything: instantiation, input extraction, and execution const builder2 = new ExpressRestApiBuilder({ runService: async (Service, context) => { const { request, session } = context; const input = { ...request.query, ...request.params, ...request.body, }; const service = new Service({ session, db: database }); return await service.run(input); }, loadSession: async (req) => ({ userId: 1 }), services: [['GET', '/users', UsersList]], }); ``` -------------------------------- ### CreateService Type Definition and Example Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Defines the CreateService type, a factory function for dependency injection. It takes a ServiceClass and RequestContext to create a new service instance, allowing for custom dependency injection logic. ```typescript type CreateService = (Service: ServiceClass, context: RequestContext) => Service; // Example usage const createService: CreateService = (Service, context) => { return new Service({ session: context.session, db: database, logger: console, }); }; ``` -------------------------------- ### Testing Express API with Supertest Source: https://context7.com/koorchik/node-chista-express/llms.txt Demonstrates how to test an Express API built with Chista Express using the supertest library. It covers testing GET, POST requests, handling 404 errors, and authentication errors. ```typescript import request from 'supertest'; import { ExpressRestApiBuilder, RestApiError } from 'chista-express'; describe('User API', () => { let builder: ExpressRestApiBuilder; const mockDb = new Map(); beforeEach(() => { mockDb.clear(); mockDb.set(1, { id: 1, name: 'Alice', email: 'alice@test.com' }); builder = new ExpressRestApiBuilder({ loadSession: async () => ({ userId: 1 }), createService: (Service) => new Service({ db: mockDb }), services: [ ['GET', '/users', UsersList], ['GET', '/users/:id', UsersShow], ['POST', '/users', UsersCreate], ], }); builder.build(); }); test('should list users', async () => { const response = await request(builder.getApp()) .get('/api/users') .expect(200); expect(response.body.success).toBe(true); expect(response.body.result).toHaveLength(1); expect(response.body.result[0].name).toBe('Alice'); }); test('should create user', async () => { const response = await request(builder.getApp()) .post('/api/users') .send({ name: 'Bob', email: 'bob@test.com' }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.result.name).toBe('Bob'); }); test('should return 404 for missing user', async () => { const response = await request(builder.getApp()) .get('/api/users/999') .expect(404); expect(response.body.success).toBe(false); expect(response.body.error.code).toBe('NOT_FOUND'); }); test('should handle auth errors', async () => { const authBuilder = new ExpressRestApiBuilder({ loadSession: async () => { throw new RestApiError({ message: 'Unauthorized' }, 401); }, createService: (Service) => new Service(), services: [['GET', '/protected', ProtectedService]], }); authBuilder.build(); await request(authBuilder.getApp()) .get('/api/protected') .expect(401); }); }); ``` -------------------------------- ### WebSocket Support with Authentication in Express Source: https://context7.com/koorchik/node-chista-express/llms.txt Demonstrates how to integrate WebSocket services using the 'WS' method. Similar to HTTP routes, WebSocket routes can be authenticated via loadSession or be public. The example shows an authenticated chat service and a public notification service. ```typescript import { ExpressRestApiBuilder } from 'chista-express'; class ChatService { constructor(private deps: { userId: number }) {} async run(input: { ws: any; roomId: string }) { const { ws, roomId } = input; ws.send(JSON.stringify({ type: 'connected', roomId, userId: this.deps.userId, })); ws.on('message', (msg: string) => { const data = JSON.parse(msg); // Broadcast to room, save to DB, etc. ws.send(JSON.stringify({ type: 'message', from: this.deps.userId, text: data.text, })); }); ws.on('close', () => { console.log(`User ${this.deps.userId} left room ${roomId}`); }); } } class PublicNotifications { async run(input: { ws: any }) { input.ws.on('message', () => { input.ws.send(JSON.stringify({ type: 'pong' })); }); } } const builder = new ExpressRestApiBuilder({ createService: (Service, ctx) => new Service({ userId: ctx.session?.userId }), loadSession: async (req) => { const token = req.query.token as string; if (!token) throw new RestApiError({ message: 'Unauthorized' }, 401); return { userId: 1 }; }, services: [ ['GET', '/users', UsersList], ['WS', '/ws/chat/:roomId', ChatService], // Authenticated WebSocket ], unauthenticatedServices: [ ['WS', '/ws/notifications', PublicNotifications], // Public WebSocket ], }); // Connect: ws://localhost:3000/api/ws/chat/room123?token=xxx ``` -------------------------------- ### Finalizing Routes and Error Handling with build() (JavaScript) Source: https://context7.com/koorchik/node-chista-express/llms.txt Illustrates the use of the `build()` method to register all defined routes and the error handler. This method can only be called once to finalize the route configuration. The example shows adding middleware before `build()`, calling `build()`, and then adding a custom error handling middleware that executes after service routes have been processed. ```javascript const builder = new ExpressRestApiBuilder({ createService: (Service) => new Service({ db }), loadSession: async () => ({ userId: 1 }), services: [['GET', '/users', UsersList]], }); const app = builder.getApp(); // Pre-route middleware app.use(cors()); // Register routes (can only be called once) builder.build(); // Post-route handlers app.use((err, req, res, next) => { console.error('Custom error handler:', err); res.status(500).json({ error: 'Something went wrong' }); }); app.listen(3000, () => console.log('Server running')); ``` -------------------------------- ### Route Configuration Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Demonstrates how to define routes using 3-tuples (method, path, service) and 4-tuples (method, path, service, options) for per-route configuration. ```APIDOC ## Route Options Routes can be defined as 3-tuples or 4-tuples. ### Standard Route (3-tuple) ```typescript ['GET', '/users', UsersList] ``` ### Route with Options (4-tuple) ```typescript ['POST', '/upload', UploadService, { middlewares: [multer().single('file')], extractInput: (ctx) => ({ ...ctx.request.body, file: ctx.request.file }), }] ``` #### Available Options | Option | Type | Description | |---------------|-------------------|------------------------------------------------| | `middlewares` | `RequestHandler[]`| Express middlewares to run before the service. | | `runService` | `RunService` | Override global `runService` for this route. | | `createService`| `CreateService` | Override global `createService` for this route.| | `extractInput`| `ExtractInput` | Override global `extractInput` for this route. | | `mapError` | `MapError` | Override global `mapError` for this route. | ``` -------------------------------- ### Initialize ExpressRestApiBuilder and Configure Middleware (TypeScript) Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Demonstrates initializing the ExpressRestApiBuilder and applying global middleware before route registration. This is useful for setting up CORS, security headers, compression, and request logging that should apply to all requests before service routes are processed. ```typescript import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; import morgan from 'morgan'; import { ExpressRestApiBuilder } from 'chista-express'; const config = { /* ... your config ... */ }; const builder = new ExpressRestApiBuilder(config); const app = builder.getApp(); // Global middleware applied BEFORE service routes app.use(cors({ origin: '*' })); app.use(helmet()); app.use(compression()); app.use(morgan('combined')); builder.build(); // Additional routes and handlers applied AFTER service routes app.use('/', express.static('public')); app.use('*', (req, res) => res.status(404).json({ error: 'Not found' })); app.listen(3000, () => { console.log('Server running on port 3000'); }); ``` -------------------------------- ### ExtractInput Type Definition and Example Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Defines the ExtractInput type, which allows customization of how input data is extracted from the RequestContext. This is useful for consolidating data from various sources like query parameters, body, and WebSocket connections. ```typescript type ExtractInput = (context: RequestContext) => Record; // Example usage const extractInput: ExtractInput = (context) => ({ ...context.request.query, ...context.request.params, ...context.request.body, ws: context.ws, }); ``` -------------------------------- ### WebSocket Support Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Explains how to implement WebSocket routes and their authentication requirements. ```APIDOC ## WebSocket Support WebSocket routes follow the same authentication pattern as HTTP routes. - Routes in `services` require authentication (`loadSession` is called). - Routes in `unauthenticatedServices` are public (`loadSession` is NOT called). ### Example WebSocket Service ```typescript class ChatService { async run({ ws, roomId, session }) { ws.on('message', (msg) => { ws.send(`Echo: ${msg}`); }); } } // WebSocket routes in services array services: [ ['GET', '/users', UsersList], ['WS', '/ws/chat/:roomId', ChatService] ] ``` ``` -------------------------------- ### Configure Routes with Options (TypeScript) Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Demonstrates how to define routes with optional configuration objects in Chista-Express. This allows for per-route customization of middlewares, input extraction, and other service-related behaviors. ```typescript const routes = [ // Standard route (3-tuple) ['GET', '/users', UsersList], // Route with options (4-tuple) ['POST', '/upload', UploadService, { middlewares: [multer().single('file')], extractInput: (ctx) => ({ ...ctx.request.body, file: ctx.request.file }), }], ]; ``` -------------------------------- ### getApp() Method Source: https://context7.com/koorchik/node-chista-express/llms.txt Retrieves the underlying Express Application instance, allowing for the addition of custom middleware before or after route registration. ```APIDOC ## getApp() Method ### Description Returns the standard Express Application instance, allowing you to add custom middleware before or after route registration. This provides full access to Express APIs with no special types or lock-in. ### Method `builder.getApp()` ### Parameters None ### Request Example ```typescript import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; import express from 'express'; // Assuming express is imported const builder = new ExpressRestApiBuilder({ /* config */ }); const app = builder.getApp(); // Middleware added BEFORE build() runs BEFORE all service routes app.use(cors({ origin: '*' })); app.use(helmet()); app.use(compression()); // Register all routes builder.build(); // Routes added AFTER build() run only if no service matched app.use('/static', express.static('public')); app.use('*', (req, res) => res.status(404).json({ error: 'Not found' })); app.listen(3000); ``` ### Response Returns the Express `app` instance. ``` -------------------------------- ### Using with Chista Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Demonstrates integrating Chista services with Chista Express, including validation and error mapping. ```APIDOC ## Using with chista The [chista](https://www.npmjs.com/package/chista) package is a great companion for building services with built-in LIVR validation and lifecycle hooks: ```bash npm install chista ``` ### Example Service with Validation ```typescript import { ServiceBase, ServiceError } from 'chista'; const validation = { name: ['required', { min_length: 1 }], email: ['required', 'email'], } as const; class UsersCreate extends ServiceBase { static validation = validation; async execute(data: { name: string; email: string }) { // Validation runs automatically before execute() return this.db.users.create(data); } } ``` ### Example Builder Configuration Map `ServiceError` to API responses: ```typescript import { ServiceError } from 'chista'; const builder = new ExpressRestApiBuilder({ createService: (Service) => new Service({ db }), mapError: (error) => { if (error instanceof ServiceError) { return new RestApiError(error.toObject(), 200); } return undefined; }, loadSession: async () => ({ userId: 1 }), services: [ ['POST', '/users', UsersCreate], ], }); ``` See the [chista example](./examples/chista) for a complete implementation. ``` -------------------------------- ### Custom API Error Handling with RestApiError (TypeScript) Source: https://context7.com/koorchik/node-chista-express/llms.txt Demonstrates how to use the RestApiError class to create structured error responses for API requests. It shows examples of handling 'Not Found' errors and 'Validation' errors with specific field details, returning errors with appropriate HTTP status codes. ```typescript import { RestApiError } from 'chista-express'; class UsersShow { constructor(private deps: { db: Map }) {} async run(input: { id: string }) { const user = this.deps.db.get(parseInt(input.id, 10)); if (!user) { throw new RestApiError( { code: 'NOT_FOUND', message: 'User not found' }, 404 ); } return user; } } // Validation error with field details class UsersCreate { async run(input: { name?: string; email?: string }) { if (!input.name || !input.email) { throw new RestApiError( { code: 'VALIDATION_ERROR', message: 'Validation failed', fields: { name: !input.name ? 'Required' : undefined, email: !input.email ? 'Required' : undefined, }, }, 400 ); } // ... create user } } // Response format for errors: // HTTP 404 // { "success": false, "error": { "code": "NOT_FOUND", "message": "User not found" } } ``` -------------------------------- ### createService vs runService Configuration Source: https://context7.com/koorchik/node-chista-express/llms.txt Compares two configuration options for ExpressRestApiBuilder: `createService` for dependency injection and automatic input extraction, and `runService` for full manual control over service instantiation and execution. ```APIDOC ## createService vs runService Configuration Two execution modes for services: `createService` for dependency injection with automatic input extraction, and `runService` for full control over service instantiation and execution. ### Option 1: createService (Recommended) Library handles input extraction and service.run() call. ```typescript const builder1 = new ExpressRestApiBuilder({ createService: (Service, context) => { return new Service({ session: context.session, db: database, logger: console, }); }, // Optional: customize input extraction extractInput: (context) => ({ ...context.request.query, ...context.request.params, ...context.request.body, clientIp: context.request.ip, }), // Optional: transform domain errors mapError: (error) => { if (error instanceof ValidationError) { return new RestApiError(error.toObject(), 400); } return undefined; }, loadSession: async (req) => ({ userId: 1 }), services: [['GET', '/users', UsersList]], }); ``` ### Option 2: runService (Full Control) You handle everything: instantiation, input extraction, and execution. ```typescript const builder2 = new ExpressRestApiBuilder({ runService: async (Service, context) => { const { request, session } = context; const input = { ...request.query, ...request.params, ...request.body, }; const service = new Service({ session, db: database }); return await service.run(input); }, loadSession: async (req) => ({ userId: 1 }), services: [['GET', '/users', UsersList]], }); ``` ``` -------------------------------- ### Streaming Uploads with Per-Route runService Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Illustrates how to use a per-route `runService` to gain access to the raw request for streaming large files, allowing for pre-streaming permission checks. ```APIDOC ## Streaming Uploads Use per-route `runService` for streaming large files and performing permission checks before streaming. ```typescript import { ExpressRestApiBuilder, RestApiError } from 'chista-express'; // Assume Database, Session, Request, UploadedFile types are defined elsewhere class LargeFileService { constructor(private deps: { session: Session; db: Database }) {} async run(input: { folderId: string; request: Request }) { // 1. CHECK PERMISSIONS FIRST const folder = await this.deps.db.folders.findById(input.folderId); if (folder.ownerId !== this.deps.session.userId) { throw new RestApiError({ message: 'Access denied' }, 403); } // 2. STREAM FILES (only if authorized) const files = await this.streamToStorage(input.request); return { files }; } private streamToStorage(request: Request): Promise { // Implementation using busboy, formidable, or other streaming parsers return Promise.resolve([]); } } const builder = new ExpressRestApiBuilder({ // ... other configurations services: [ ['POST', '/folders/:folderId/upload', LargeFileService, { runService: async (Service, context) => { const service = new Service({ session: context.session, db, }); return service.run({ ...context.request.params, request: context.request, // Pass raw request for streaming }); }, }], ], }); ``` This pattern ensures that permissions are checked before any file streaming occurs, preventing wasted bandwidth on unauthorized requests. ``` -------------------------------- ### ExpressRestApiBuilder Constructor and Service Registration Source: https://context7.com/koorchik/node-chista-express/llms.txt Demonstrates how to initialize the ExpressRestApiBuilder with configuration options and register services for both authenticated and unauthenticated routes. ```APIDOC ## ExpressRestApiBuilder Constructor ### Description The main class that wraps Express and provides the builder pattern for API construction. It initializes Express with JSON parsing and WebSocket support, validates all configuration options, and prepares the application for route registration. ### Method `new ExpressRestApiBuilder(options)` ### Parameters #### Constructor Options - **apiBaseUrl** (string) - Optional - Base URL for authenticated routes. - **unauthenticatedApiBaseUrl** (string) - Optional - Base URL for public routes. - **logger** (object) - Optional - Logger instance (e.g., console). - **jsonParser** (object) - Optional - Configuration for the JSON body parser (e.g., `{ limit: '10mb' }`). - **createService** (function) - Required - Factory function to create service instances with dependencies. - **loadSession** (function) - Optional - Asynchronous function to load session data from the request. - **services** (Array<[string, string, class]>) - Optional - Array of authenticated routes to register (e.g., `['GET', '/users', UsersList]`). - **unauthenticatedServices** (Array<[string, string, class]>) - Optional - Array of unauthenticated routes to register (e.g., `['GET', '/health', HealthCheck]`). ### Request Example (Conceptual) ```typescript import { ExpressRestApiBuilder, RestApiError } from 'chista-express'; // Define service classes (UsersList, UsersCreate, HealthCheck) const db = new Map(); const builder = new ExpressRestApiBuilder({ apiBaseUrl: '/api', unauthenticatedApiBaseUrl: '/api/public', logger: console, jsonParser: { limit: '10mb' }, createService: (Service, context) => { return new Service({ db, userId: context.session?.userId }); }, loadSession: async (req) => { const token = req.headers['x-access-token']; if (!token) throw new RestApiError({ message: 'Unauthorized' }, 401); return { userId: 1, token }; }, services: [ ['GET', '/users', UsersList], ['POST', '/users', UsersCreate], ], unauthenticatedServices: [ ['GET', '/health', HealthCheck], ], }); ``` ### Response Returns an instance of `ExpressRestApiBuilder`. ``` -------------------------------- ### Service Class Structure in TypeScript Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Defines the standard pattern for services, which are classes with a `run()` method. The constructor accepts dependencies like database connections and user IDs, while the `run()` method processes input and performs the service's logic. ```typescript class UserCreate { constructor(private deps: { db: Database; userId: number }) {} async run(input: { name: string; email: string }) { return this.deps.db.insert('users', { ...input, createdBy: this.deps.userId }); } } ``` -------------------------------- ### File Uploads with Multer Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Shows how to integrate multer for handling file uploads, including single file and multiple file uploads. ```APIDOC ## File Uploads Supports file uploads using per-route middlewares like multer. ### Installation ```bash npm install multer @types/multer ``` ### Basic File Upload Example ```typescript import { ExpressRestApiBuilder, RestApiError } from 'chista-express'; import multer from 'multer'; const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024 }, // 5MB }); class AvatarUpload { async run(input: { file?: Express.Multer.File }) { if (!input.file) { throw new RestApiError({ message: 'No file uploaded', code: 'NO_FILE' }, 400); } // Process the uploaded file return { filename: input.file.originalname, size: input.file.size, mimetype: input.file.mimetype, }; } } const builder = new ExpressRestApiBuilder({ // ... other configurations services: [ ['POST', '/avatar', AvatarUpload, { middlewares: [upload.single('avatar')], }], ['POST', '/documents', DocumentsUpload, { middlewares: [upload.array('documents', 10)], }], ], }); ``` The default `extractInput` automatically handles `file` and `files` from multer. ``` -------------------------------- ### Controlling Middleware Order with Builder Pattern (TypeScript) Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Illustrates the core benefit of the builder pattern in chista-express by showing how to apply global middleware before route registration and add catch-all routes or static file serving after. This ensures correct execution order for middleware like CORS, authentication, and error handling. ```typescript import express from 'express'; import cors from 'cors'; import compression from 'compression'; import { ExpressRestApiBuilder } from 'chista-express'; const config = { /* ... your config ... */ }; const builder = new ExpressRestApiBuilder(config); const app = builder.getApp(); // 1. Add global middleware BEFORE routes app.use(cors()); app.use(compression()); // 2. Register service routes builder.build(); // 3. Add catch-all routes AFTER services app.use('/static', express.static('public')); app.use('*', (req, res) => res.status(404).send('Not found')); ``` -------------------------------- ### Define WebSocket Service with TypeScript Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Illustrates how to define a WebSocket service using TypeScript within the chista-express framework. It shows how to handle incoming messages and echo them back to the client. This approach requires the 'chista-express' and 'chista' packages. ```typescript class ChatService { async run({ ws, roomId, session }) { ws.on('message', (msg) => { ws.send(`Echo: ${msg}`); }); } } // WebSocket routes in services array services: [ ['GET', '/users', UsersList], ['WS', '/ws/chat/:roomId', ChatService] ] ``` -------------------------------- ### Service Execution with runService in TypeScript Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Illustrates using `runService` for full control over service execution, including manual instantiation and input handling. This provides maximum flexibility for complex scenarios. ```typescript const builder = new ExpressRestApiBuilder({ logger, runService: async (Service, context) => { const { request, session } = context; const service = new Service({ session, db }); const input = { ...request.query, ...request.params, ...request.body }; return await service.run(input); }, services: [...] }); ``` -------------------------------- ### Basic File Upload with Multer (TypeScript) Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Shows a basic file upload implementation using the multer middleware with Chista-Express. It includes setting up multer for memory storage and limits, defining a service to handle the uploaded file, and configuring the route. ```bash npm install multer @types/multer ``` ```typescript import { ExpressRestApiBuilder, RestApiError } from 'chista-express'; import multer from 'multer'; const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024 }, // 5MB }); class AvatarUpload { async run(input: { file?: Express.Multer.File }) { if (!input.file) { throw new RestApiError({ message: 'No file uploaded', code: 'NO_FILE' }, 400); } // Upload to cloud storage, save to disk, etc. return { filename: input.file.originalname, size: input.file.size, mimetype: input.file.mimetype, }; } } const builder = new ExpressRestApiBuilder({ createService: (Service, ctx) => new Service({ session: ctx.session }), loadSession: async () => ({ userId: 1 }), services: [ // Regular JSON route ['GET', '/users', UsersList], // File upload route with multer middleware ['POST', '/avatar', AvatarUpload, { middlewares: [upload.single('avatar')], }], // Multiple files ['POST', '/documents', DocumentsUpload, { middlewares: [upload.array('documents', 10)], }], ], }); ``` -------------------------------- ### build() Method Source: https://context7.com/koorchik/node-chista-express/llms.txt Registers all defined routes and the error handler. This method should be called once to finalize the API configuration. ```APIDOC ## build() Method ### Description Registers all routes and the error handler. This method can only be called once and finalizes the route configuration. After calling `build()`, any middleware added to the app will execute after service routes. ### Method `builder.build()` ### Parameters None ### Request Example ```typescript import { ExpressRestApiBuilder } from 'chista-express'; import cors from 'cors'; // Assuming UsersList service is defined elsewhere class UsersList { async run() { return []; } } const builder = new ExpressRestApiBuilder({ createService: (Service) => new Service({ db: new Map() }), loadSession: async () => ({ userId: 1 }), services: [['GET', '/users', UsersList]], }); const app = builder.getApp(); // Pre-route middleware app.use(cors()); // Register routes (can only be called once) builder.build(); // Post-route handlers app.use((err, req, res, next) => { console.error('Custom error handler:', err); res.status(500).json({ error: 'Something went wrong' }); }); app.listen(3000, () => console.log('Server running')); ``` ### Response None. This method modifies the application state by registering routes and handlers. ``` -------------------------------- ### WebSocket-Aware Middleware Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Demonstrates how to use the `skipForWebSocket` utility to prevent middleware that interferes with the WebSocket handshake from being applied to WebSocket upgrade requests. ```APIDOC ## WebSocket-Aware Middleware Use `skipForWebSocket` to conditionally apply middleware, ensuring it doesn't interfere with WebSocket upgrade requests. ```typescript import { ExpressRestApiBuilder, skipForWebSocket } from 'chista-express'; import basicAuth from 'express-basic-auth'; const builder = new ExpressRestApiBuilder({ /* ... */ }); const app = builder.getApp(); // Basic auth can break WebSocket handshake, so skip it for WS requests app.use(skipForWebSocket(basicAuth({ users: { admin: 'password' }, challenge: true }))); builder.build(); ``` This ensures that middleware like `express-basic-auth` is only applied to standard HTTP requests and not to WebSocket upgrade requests. ``` -------------------------------- ### Streaming Uploads with Per-Route runService (TypeScript) Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Illustrates how to handle large file uploads by streaming them directly to storage after performing permission checks. This uses a per-route `runService` to gain access to the raw request object, enabling the use of streaming parsers like busboy or formidable. ```typescript class LargeFileService { constructor(private deps: { session: Session; db: Database }) {} async run(input: { folderId: string; request: Request }) { // 1. CHECK PERMISSIONS FIRST (before any streaming) const folder = await this.deps.db.folders.findById(input.folderId); if (folder.ownerId !== this.deps.session.userId) { throw new RestApiError({ message: 'Access denied' }, 403); } // 2. STREAM FILES (only if authorized) const files = await this.streamToStorage(input.request); return { files }; } private streamToStorage(request: Request): Promise { // Use busboy, formidable, or other streaming parsers } } const builder = new ExpressRestApiBuilder({ createService: (Service, ctx) => new Service({ session: ctx.session, db }), loadSession: async (req) => validateToken(req.headers.authorization), services: [ // Standard routes use global createService ['GET', '/folders', FoldersList], // Streaming upload: per-route runService for raw request access ['POST', '/folders/:folderId/upload', LargeFileService, { runService: async (Service, context) => { const service = new Service({ session: context.session, db, }); return service.run({ ...context.request.params, request: context.request, // Pass raw request for streaming }); }, }], ], }); ``` -------------------------------- ### ExpressRestApiBuilder Constructor and Service Definition (TypeScript) Source: https://context7.com/koorchik/node-chista-express/llms.txt Defines service classes (UsersList, UsersCreate, HealthCheck) and initializes the ExpressRestApiBuilder with configuration. This includes API base URLs, logger, JSON parser settings, a custom service creation function with dependency injection, session loading logic, and the registration of authenticated and unauthenticated services. ```typescript import { ExpressRestApiBuilder, RestApiError } from 'chista-express'; // Define a service class class UsersList { constructor(private deps: { db: Map }) {} async run() { return Array.from(this.deps.db.values()); } } class UsersCreate { constructor(private deps: { db: Map }) {} async run(input: { name: string; email: string }) { if (!input.name || !input.email) { throw new RestApiError( { code: 'VALIDATION_ERROR', message: 'Name and email required' }, 400 ); } const id = this.deps.db.size + 1; const user = { id, ...input }; this.deps.db.set(id, user); return user; } } class HealthCheck { async run() { return { status: 'ok', timestamp: new Date().toISOString() }; } } // Create builder with full configuration const db = new Map(); const builder = new ExpressRestApiBuilder({ apiBaseUrl: '/api', // Base URL for authenticated routes unauthenticatedApiBaseUrl: '/api/public', // Base URL for public routes logger: console, jsonParser: { limit: '10mb' }, // JSON body parser config createService: (Service, context) => { return new Service({ db, userId: context.session?.userId }); }, loadSession: async (req) => { const token = req.headers['x-access-token']; if (!token) throw new RestApiError({ message: 'Unauthorized' }, 401); return { userId: 1, token }; }, services: [ ['GET', '/users', UsersList], ['POST', '/users', UsersCreate], ], unauthenticatedServices: [ ['GET', '/health', HealthCheck], ], }); ``` -------------------------------- ### skipForWebSocket Helper Source: https://context7.com/koorchik/node-chista-express/llms.txt Introduces the `skipForWebSocket` helper function, which allows specific middleware to be bypassed for WebSocket upgrade requests. This is particularly useful for middleware like basic authentication that can interfere with WebSocket handshakes. ```APIDOC ## skipForWebSocket Helper ### Description A middleware wrapper that bypasses the wrapped middleware for WebSocket upgrade requests. This is useful for middleware like basic auth that can break WebSocket handshakes. ### Usage Wrap the middleware that should be skipped for WebSocket requests with `skipForWebSocket()`. ### Parameters #### `middleware` - **middleware** (function) - The Express middleware function to conditionally apply. ### Example ```typescript import { ExpressRestApiBuilder, skipForWebSocket } from 'chista-express'; import basicAuth from 'express-basic-auth'; const builder = new ExpressRestApiBuilder({ /* config */ }); const app = builder.getApp(); // Basic auth breaks WebSocket handshake - skip it for WS requests app.use(skipForWebSocket(basicAuth({ users: { admin: 'secret' }, challenge: true, }))); builder.build(); app.listen(3000); ``` ``` -------------------------------- ### Basic ExpressRestApiBuilder Configuration in TypeScript Source: https://github.com/koorchik/node-chista-express/blob/master/README.md Configures the ExpressRestApiBuilder with essential settings like API base URLs and request body size limits. It allows customization of the API's structure and request handling. ```typescript const builder = new ExpressRestApiBuilder({ apiBaseUrl: '/api', // Base URL for authenticated routes (default: '/api') unauthenticatedApiBaseUrl: '/api/public', // Base URL for unauthenticated routes (default: apiBaseUrl + '/public') logger: yourLogger, // Optional: Logger with info(string) and error(string) jsonParser: { limit: '10mb' } // Optional: default value is 1mb }); ```