# Hono Server Development Guidelines This document provides specific guidelines for developing the Hono-based backend server in this monorepo. ## API Development Standards ### Directory Structure ``` apps/server/src/api/ ├── v2/ # Current API version │ ├── chats/ # Feature folder │ │ ├── index.ts # Route definitions │ │ ├── handler.ts # Main handler (if single) │ │ ├── create-chat.ts # Individual handlers │ │ └── services/ # Business logic │ └── security/ # Another feature │ ├── index.ts │ ├── get-workspace-settings.ts │ └── update-workspace-settings.ts └── healthcheck.ts # Non-versioned endpoints ``` ### Route Definition Pattern Always follow this pattern in `index.ts`: ```typescript import { RequestSchema } from '@buster/server-shared/feature'; import { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono'; import { requireAuth } from '../../../middleware/auth'; import '../../../types/hono.types'; import { HTTPException } from 'hono/http-exception'; import { handlerFunction } from './handler-file'; const app = new Hono() // Apply authentication middleware .use('*', requireAuth) // GET endpoint (no body validation) .get('/endpoint', async (c) => { const user = c.get('busterUser'); const response = await handlerFunction(user); return c.json(response); }) // POST/PATCH/PUT endpoint (with body validation) .post('/endpoint', zValidator('json', RequestSchema), async (c) => { const request = c.req.valid('json'); const user = c.get('busterUser'); const response = await handlerFunction(request, user); return c.json(response); }) // Error handling .onError((e, c) => { if (e instanceof HTTPException) { return e.getResponse(); } throw new HTTPException(500, { message: 'Internal server error', }); }); export default app; ``` ### Handler Function Pattern Each handler must: 1. Be in its own file 2. Be a pure async function 3. Accept typed parameters 4. Return typed responses 5. Handle business logic or delegate to services ```typescript import type { FeatureRequest, FeatureResponse } from '@buster/server-shared/feature'; import type { User } from '@buster/database'; export async function featureHandler( request: FeatureRequest, user: User ): Promise { // TODO: Implement business logic // For complex logic, delegate to service functions // const result = await featureService.process(request, user); return { // Response data matching FeatureResponse type }; } ``` ### Type Safety Requirements 1. **Request/Response Types**: Define all types in `@buster/server-shared` ```typescript // In @buster/server-shared/feature/requests.ts export const FeatureRequestSchema = z.object({ field: z.string(), }); export type FeatureRequest = z.infer; ``` 2. **Validation**: Always use `zValidator` for request body validation ```typescript .post('/endpoint', zValidator('json', RequestSchema), async (c) => { const request = c.req.valid('json'); // Fully typed }) ``` 3. **User Type**: Always use `User` from `@buster/database` ```typescript import type { User } from '@buster/database'; ``` ### Authentication Pattern 1. Apply `requireAuth` middleware to all protected routes 2. Extract user with `c.get('busterUser')` 3. Pass user to handler functions ```typescript const app = new Hono() .use('*', requireAuth) // Protects all routes in this app .get('/protected', async (c) => { const user = c.get('busterUser'); // Type: User from @buster/database // Use user in handler }); ``` ### Error Handling 1. **Custom Errors**: Create domain-specific error classes ```typescript export class FeatureError extends Error { constructor( public code: string, message: string, public statusCode: number ) { super(message); } } ``` 2. **Error Responses**: Handle in route error handler ```typescript .onError((e, c) => { if (e instanceof FeatureError) { return c.json({ error: e.message, code: e.code }, e.statusCode); } // Default error handling }) ``` ### Testing Guidelines 1. **Unit Tests**: Test handlers in isolation ```typescript // handler.test.ts describe('featureHandler', () => { it('should process valid request', async () => { const mockUser = { id: '123' } as User; const request = { field: 'value' }; const result = await featureHandler(request, mockUser); expect(result).toMatchObject({ // Expected response }); }); }); ``` 2. **Integration Tests**: Test full API routes ```typescript // index.test.ts import { testClient } from 'hono/testing'; ``` ### Best Practices 1. **Separation of Concerns** - Routes: Handle HTTP concerns only - Handlers: Coordinate business logic - Services: Implement business logic - Repositories: Handle data access 2. **Consistent Naming** - Handler files: `verb-resource.ts` (e.g., `get-user.ts`, `update-settings.ts`) - Handler functions: `verbResourceHandler` (e.g., `getUserHandler`) - Service files: `resource-service.ts` 3. **Response Validation** - Consider validating responses in development - Use `.safeParse()` for critical endpoints 4. **Logging** - Log errors with context - Use appropriate log levels - Include user context where relevant 5. **Performance** - Keep handlers lightweight - Delegate heavy computation to background jobs - Monitor response times ## Common Patterns ### Pagination ```typescript // In request schema export const ListRequestSchema = z.object({ page: z.number().min(1).default(1), limit: z.number().min(1).max(100).default(20), }); // In handler const offset = (request.page - 1) * request.limit; ``` ### Query Parameters ```typescript // Define query schema in server-shared export const ListItemsQuerySchema = z.object({ search: z.string().optional(), status: z.enum(['active', 'inactive', 'all']).default('all'), page: z.coerce.number().min(1).default(1), limit: z.coerce.number().min(1).max(100).default(20), sortBy: z.enum(['created_at', 'name', 'updated_at']).default('created_at'), sortOrder: z.enum(['asc', 'desc']).default('desc'), }); export type ListItemsQuery = z.infer; // Route definition with query validation .get('/items', zValidator('query', ListItemsQuerySchema), async (c) => { const query = c.req.valid('query'); // Fully typed as ListItemsQuery const user = c.get('busterUser'); const response = await listItemsHandler(query, user); return c.json(response); }) // Handler receives typed query export async function listItemsHandler( query: ListItemsQuery, user: User ): Promise { const offset = (query.page - 1) * query.limit; // Use query.search, query.status, etc. with full type safety } ``` ### Path Parameters ```typescript // Define param schema export const ItemParamsSchema = z.object({ item_id: z.string().uuid(), }); export type ItemParams = z.infer; // Route with path param validation .get('/items/:item_id', zValidator('param', ItemParamsSchema), async (c) => { const params = c.req.valid('param'); // Typed as ItemParams const user = c.get('busterUser'); const response = await getItemHandler(params.item_id, user); return c.json(response); }) // Handler receives typed param export async function getItemHandler( itemId: string, user: User ): Promise { // itemId is guaranteed to be a valid UUID } ``` ### Combined Parameters (Path + Query + Body) ```typescript // Define schemas export const UpdateItemParamsSchema = z.object({ item_id: z.string().uuid(), }); export const UpdateItemQuerySchema = z.object({ validate: z.coerce.boolean().default(true), }); export const UpdateItemBodySchema = z.object({ name: z.string().min(1).max(255), description: z.string().optional(), status: z.enum(['active', 'inactive']).optional(), }); // Route with multiple validations .patch( '/items/:item_id', zValidator('param', UpdateItemParamsSchema), zValidator('query', UpdateItemQuerySchema), zValidator('json', UpdateItemBodySchema), async (c) => { const params = c.req.valid('param'); const query = c.req.valid('query'); const body = c.req.valid('json'); const user = c.get('busterUser'); const response = await updateItemHandler( params.item_id, body, { validate: query.validate }, user ); return c.json(response); } ) // Handler with all parameters typed export async function updateItemHandler( itemId: string, data: UpdateItemBody, options: { validate: boolean }, user: User ): Promise { if (options.validate) { // Perform validation } // Update logic } ``` ### Important Notes on Type Coercion - Use `z.coerce.number()` for numeric query params (they come as strings) - Use `z.coerce.boolean()` for boolean query params - Path params are always strings, validate format with `.uuid()`, `.regex()`, etc. - Query arrays need special handling: `ids: z.array(z.string()).or(z.string()).transform(v => Array.isArray(v) ? v : [v])` ### Background Jobs ```typescript // Queue job without waiting import { tasks } from '@trigger.dev/sdk/v3'; await tasks.trigger('job-name', { data }); // Return immediately, don't await job completion ```