From e44cda8f10f9a5952f5eef5c38e8d5e16df709bc Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Thu, 17 Jul 2025 12:34:48 -0600 Subject: [PATCH] Update documentation for future background agents --- apps/server/.cursor/global.mdc | 196 +++++++++++++++++ apps/server/README.md | 235 +++++++++++++++++++++ apps/server/src/api/v2/organization/PUT.ts | 41 +--- apps/server/src/utils/response.ts | 54 +++++ 4 files changed, 488 insertions(+), 38 deletions(-) create mode 100644 apps/server/.cursor/global.mdc diff --git a/apps/server/.cursor/global.mdc b/apps/server/.cursor/global.mdc new file mode 100644 index 000000000..e0706df7e --- /dev/null +++ b/apps/server/.cursor/global.mdc @@ -0,0 +1,196 @@ +--- +globs: src/* +alwaysApply: false +--- + +# Buster Server API Patterns + +## Modular Route Architecture + +### File Structure Pattern +Create separate files for each HTTP method within feature directories: +``` +src/api/v2/[resource]/ +├── GET.ts # GET operations +├── PUT.ts # PUT operations +├── POST.ts # POST operations +├── DELETE.ts # DELETE operations +└── index.ts # Barrel export combining all methods +``` + +### Barrel Export Pattern (index.ts) +Always combine route methods through barrel exports with global middleware: +```typescript +import { Hono } from 'hono'; +import { requireAuth } from '../../../middleware/auth'; +import GET from './GET'; +import PUT from './PUT'; + +const app = new Hono() + .use('*', requireAuth) // Global auth for ALL methods + .route('/', GET) // Mount individual handlers + .route('/', PUT); + +export default app; +``` + +## Middleware Patterns + +### Layered Authentication +1. **Global Level**: Apply `requireAuth` in index.ts for ALL routes +2. **Method Level**: Apply specific authorization in individual files: + - `requireOrganization` - User must belong to organization + - `requireOrganizationAdmin` - User must be organization admin + - `requireWorkspaceAccess` - User must have workspace access + +```typescript +// In individual route files (GET.ts, PUT.ts, etc.) +const app = new Hono() + .use('*', requireOrganizationAdmin) // Method-specific auth + .put('/', zValidator('json', RequestSchema), handler); +``` + +## Type Safety Requirements + +### Schema Definition Pattern +Define all schemas in `@buster/server-shared/[feature]`: +```typescript +// @buster/server-shared/organization/types.ts +export const UpdateOrganizationRequestSchema = z.object({ + colorPalette: z.array(z.string()).optional(), +}); + +export type UpdateOrganizationRequest = z.infer; +export type UpdateOrganizationResponse = { + id: string; + // ... fields +}; +``` + +### Import Pattern +**CRITICAL**: Always use `type` keyword for type imports to minimize build size: +```typescript +import type { RequestType, ResponseType } from '@buster/server-shared/feature'; +import { RequestSchema } from '@buster/server-shared/feature'; +``` + +### Validation Pattern +Use Hono's `zValidator` for ALL request validation: +```typescript +import { zValidator } from '@hono/zod-validator'; + +const app = new Hono() + .put('/', zValidator('json', RequestSchema), async (c) => { + const request = c.req.valid('json'); // Fully typed + // handler logic + }); +``` + +## Database Interaction Rules + +### Required Pattern +**ALL database operations MUST go through `@buster/database` package:** +```typescript +// ✅ CORRECT +import { getOrganization, updateOrganization } from '@buster/database'; +const org = await getOrganization({ organizationId }); + +// ❌ FORBIDDEN - No direct database queries +// const result = await db.query('SELECT...'); +``` + +## Error Handling Strategy + +### Use Shared Error Utilities +Use standardized error handling from `@/utils/response` for consistency: +```typescript +import { standardErrorHandler } from '../../utils/response'; + +// Basic usage - handles all error types automatically +.onError(standardErrorHandler); + +// With custom message for specific errors +.onError((e, c) => standardErrorHandler(e, c, 'Custom error message')); +``` + +### Available Error Utilities +- `standardErrorHandler(error, context, customMessage?)` - Complete error handler that returns Hono response for all error types +- `handleZodError(zodError)` - Formats Zod validation errors with detailed issues +- `errorResponse(message, status)` - Creates HTTPException for throwing errors +- `notFoundResponse(resource)` - Standard 404 error +- `unauthorizedResponse(message)` - Standard 401 error + +## Required Handler Structure + +### Handler Function Pattern +```typescript +import { errorResponse } from '../../utils/response'; + +async function handlerFunction( + resourceId: string, + request: RequestType, + user: User +): Promise { + try { + // Database operations through @buster/database only + const result = await databaseFunction({ resourceId, ...request }); + return result; + } catch (error) { + // Log with context + console.error('Error in handler:', { + resourceId, + userId: user.id, + error: error instanceof Error ? error.message : error, + }); + + // Re-throw Zod errors for route handler + if (error instanceof z.ZodError) { + throw error; + } + + // Use shared error response utility + throw errorResponse('Operation failed', 500); + } +} +``` + +### Route Definition Pattern +```typescript +import { zValidator } from '@hono/zod-validator'; +import { Hono } from 'hono'; +import { standardErrorHandler, errorResponse } from '../../utils/response'; + +const app = new Hono() + .use('*', /* appropriate middleware */) + .method('/', zValidator('json', RequestSchema), async (c) => { + const request = c.req.valid('json'); + const user = c.get('busterUser'); + const userOrg = c.get('userOrganizationInfo'); + + const response = await handlerFunction( + userOrg.organizationId, + request, + user + ); + + return c.json(response); + }) + .onError(standardErrorHandler); + // Or with custom message: .onError((e, c) => standardErrorHandler(e, c, 'Custom message')); + +export default app; +``` + +## Checklist for New Routes + +- [ ] Create separate file for each HTTP method (GET.ts, PUT.ts, etc.) +- [ ] Define request/response types in `@buster/server-shared` +- [ ] Import types with `type` keyword +- [ ] Use `zValidator` for request validation +- [ ] Apply appropriate middleware (global + method-specific) +- [ ] Route database operations through `@buster/database` +- [ ] Import and use `standardErrorHandler` from `@/utils/response` +- [ ] Implement error handling with `.onError(standardErrorHandler)` +- [ ] Use `errorResponse` for throwing consistent errors in handlers +- [ ] Combine methods in index.ts with barrel export pattern +- [ ] Add global `requireAuth` middleware in index.ts diff --git a/apps/server/README.md b/apps/server/README.md index 2a7cecabf..713939e4a 100644 --- a/apps/server/README.md +++ b/apps/server/README.md @@ -90,6 +90,235 @@ Each package in `@/packages` typically contains: This architecture keeps the server layer focused on HTTP routing, middleware, and request/response handling while delegating domain logic to specialized packages. +## API Route Patterns + +We follow a specific modular pattern for organizing API routes that promotes maintainability, type safety, and clear separation of concerns. + +### Modular Route Structure + +Each HTTP method is defined in its own dedicated file and exported through a barrel pattern: + +``` +src/api/v2/organization/ +├── GET.ts # Handles GET /organization +├── PUT.ts # Handles PUT /organization +├── POST.ts # Handles POST /organization (if needed) +├── DELETE.ts # Handles DELETE /organization (if needed) +└── index.ts # Barrel export that combines all methods +``` + +**Example Implementation:** + +```typescript +// GET.ts - Individual route handler +import { getOrganization } from '@buster/database'; +import type { GetOrganizationResponse } from '@buster/server-shared/organization'; +import { Hono } from 'hono'; +import { requireOrganization } from '../../../middleware/auth'; + +const app = new Hono() + .use('*', requireOrganization) + .get('/', async (c) => { + const userOrg = c.get('userOrganizationInfo'); + + const organization: GetOrganizationResponse = await getOrganization({ + organizationId: userOrg.organizationId, + }); + + return c.json(organization); + }); + +export default app; +``` + +```typescript +// index.ts - Barrel export combining all methods +import { Hono } from 'hono'; +import { requireAuth } from '../../../middleware/auth'; +import GET from './GET'; +import PUT from './PUT'; + +const app = new Hono() + .use('*', requireAuth) // Global middleware for all methods + .route('/', GET) // Mount individual route handlers + .route('/', PUT); + +export default app; +``` + +### Middleware Architecture + +We use a layered middleware approach for authentication and authorization: + +1. **Global Authentication**: Applied at the barrel export level (`requireAuth`) +2. **Method-Specific Authorization**: Applied in individual route files (`requireOrganization`, `requireOrganizationAdmin`) + +```typescript +// index.ts - Global auth for all methods +const app = new Hono() + .use('*', requireAuth) // ALL routes require authentication + .route('/', GET) + .route('/', PUT); + +// PUT.ts - Additional admin requirement for updates +const app = new Hono() + .use('*', requireOrganizationAdmin) // PUT requires admin privileges + .put('/', zValidator('json', UpdateOrganizationRequestSchema), async (c) => { + // Handler logic + }); +``` + +### Type Safety and Validation + +All endpoints must define strict request and response types using our established patterns: + +#### 1. Schema Definition in `@buster/server-shared` + +```typescript +// @buster/server-shared/organization/types.ts +import { z } from 'zod'; + +export const UpdateOrganizationRequestSchema = z.object({ + colorPalette: z.array(z.string()).optional(), + // ... other fields +}); + +export type UpdateOrganizationRequest = z.infer; +export type UpdateOrganizationResponse = { + id: string; + name: string; + // ... organization fields +}; +``` + +#### 2. Import Types with `type` Keyword + +**Critical**: Always use the `type` keyword when importing types to minimize build size: + +```typescript +import type { + UpdateOrganizationRequest, + UpdateOrganizationResponse +} from '@buster/server-shared/organization'; +import { UpdateOrganizationRequestSchema } from '@buster/server-shared/organization'; +``` + +#### 3. Use Hono's `zValidator` for Request Validation + +```typescript +import { zValidator } from '@hono/zod-validator'; + +const app = new Hono() + .put('/', zValidator('json', UpdateOrganizationRequestSchema), async (c) => { + const request = c.req.valid('json'); // Fully typed request + // Handler logic + }); +``` + +### Database Interaction Pattern + +**Important**: All database interactions must go through the `@buster/database` package. Never interact with the database directly in route handlers. + +```typescript +// ✅ Correct - Use database package functions +import { getOrganization, updateOrganization } from '@buster/database'; + +const organization = await getOrganization({ organizationId }); +await updateOrganization({ organizationId, ...request }); + +// ❌ Incorrect - Direct database queries +// const result = await db.query('SELECT * FROM organizations...'); +``` + +### Error Handling Strategy + +We pass detailed errors straight through to the client to make debugging easier for developers. Use the shared error handling utilities from `@/utils/response`: + +```typescript +import { standardErrorHandler } from '../../utils/response'; + +// Basic usage - handles all error types automatically +.onError(standardErrorHandler); + +// With custom message for specific errors +.onError((e, c) => standardErrorHandler(e, c, 'Failed to update organization settings')); +``` + +**Available Error Utilities:** +- `standardErrorHandler(error, context, customMessage?)` - Complete error handler that returns Hono response for all error types +- `handleZodError(zodError)` - Specifically formats Zod validation errors with detailed issues +- `errorResponse(message, status)` - Creates HTTPException for throwing errors +- `notFoundResponse(resource)` - Standard 404 error +- `unauthorizedResponse(message)` - Standard 401 error + +### Complete Route Handler Template + +```typescript +import { /* database functions */ } from '@buster/database'; +import type { User } from '@buster/database'; +import type { + RequestType, + ResponseType +} from '@buster/server-shared/feature'; +import { RequestSchema } from '@buster/server-shared/feature'; +import { zValidator } from '@hono/zod-validator'; +import { Hono } from 'hono'; +import { z } from 'zod'; +import { /* middleware */ } from '../../../middleware/auth'; +import { standardErrorHandler, errorResponse } from '../../utils/response'; + +/** + * Handler function with proper error handling + */ +async function handlerFunction( + resourceId: string, + request: RequestType, + user: User +): Promise { + try { + // Database operations through @buster/database + const result = await databaseFunction({ resourceId, ...request }); + return result; + } catch (error) { + console.error('Error in handler:', { + resourceId, + userId: user.id, + error: error instanceof Error ? error.message : error, + }); + + // Re-throw Zod errors for route error handler + if (error instanceof z.ZodError) { + throw error; + } + + // Use shared error response utility + throw errorResponse('Operation failed', 500); + } +} + +const app = new Hono() + .use('*', /* appropriate middleware */) + .put('/', zValidator('json', RequestSchema), async (c) => { + const request = c.req.valid('json'); + const user = c.get('busterUser'); + const userOrg = c.get('userOrganizationInfo'); + + const response = await handlerFunction( + userOrg.organizationId, + request, + user + ); + + return c.json(response); + }) + .onError(standardErrorHandler); + +// Or with custom error message for this specific route +// .onError((e, c) => standardErrorHandler(e, c, 'Failed to update organization')); + +export default app; +``` + ## Best Practices - Use TypeScript for type safety @@ -98,3 +327,9 @@ This architecture keeps the server layer focused on HTTP routing, middleware, an - Use proper error handling with Hono's error utilities - Leverage Hono's built-in validation and serialization - Follow RESTful conventions for API endpoints +- **Always use the modular route pattern** with separate files per HTTP method +- **Import types with `type` keyword** to minimize build size +- **Use `zValidator` for all request validation** +- **Route all database interactions through `@buster/database`** +- **Use shared error utilities from `@/utils/response`** for consistent error handling +- **Pass detailed errors to clients** for easier debugging diff --git a/apps/server/src/api/v2/organization/PUT.ts b/apps/server/src/api/v2/organization/PUT.ts index 1f2a7163a..d20d013bb 100644 --- a/apps/server/src/api/v2/organization/PUT.ts +++ b/apps/server/src/api/v2/organization/PUT.ts @@ -7,9 +7,9 @@ import type { import { UpdateOrganizationRequestSchema } from '@buster/server-shared/organization'; import { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono'; -import { HTTPException } from 'hono/http-exception'; import { z } from 'zod'; import { requireOrganizationAdmin } from '../../../middleware/auth'; +import { errorResponse, standardErrorHandler } from '../../../utils/response'; /** * Updates organization settings @@ -46,9 +46,7 @@ async function updateOrganizationHandler( throw error; } - throw new HTTPException(500, { - message: 'Failed to update organization', - }); + throw errorResponse('Failed to update organization', 500); } } @@ -70,39 +68,6 @@ const app = new Hono() return c.json(response); }) - .onError((e, c) => { - // Handle Zod validation errors with detailed information - if (e instanceof z.ZodError) { - return c.json( - { - error: 'Validation Error', - message: 'Invalid request data', - issues: e.issues.map((issue) => ({ - path: issue.path.join('.'), - message: issue.message, - code: issue.code, - })), - }, - 400 - ); - } - - // Handle HTTP exceptions - if (e instanceof HTTPException) { - return e.getResponse(); - } - - // Log unexpected errors - console.error('Unhandled error in organization PUT:', e); - - // Return generic error for unexpected issues - return c.json( - { - error: 'Internal Server Error', - message: 'Failed to update organization', - }, - 500 - ); - }); + .onError(standardErrorHandler); export default app; diff --git a/apps/server/src/utils/response.ts b/apps/server/src/utils/response.ts index 4b5eca016..2482f73bc 100644 --- a/apps/server/src/utils/response.ts +++ b/apps/server/src/utils/response.ts @@ -1,4 +1,6 @@ +import type { Context } from 'hono'; import { HTTPException } from 'hono/http-exception'; +import { z } from 'zod'; import type { ErrorResponse } from '../types/errors.types'; export const errorResponse = ( @@ -27,3 +29,55 @@ export const unauthorizedResponse = (message = 'Unauthorized') => { message, } satisfies ErrorResponse); }; + +/** + * Handles Zod validation errors with detailed issue information + * Returns a JSON response with validation error details for easier debugging + */ +export const handleZodError = (error: z.ZodError) => { + return { + error: 'Validation Error', + message: 'Invalid request data', + issues: error.issues.map((issue) => ({ + path: issue.path.join('.'), + message: issue.message, + code: issue.code, + })), + }; +}; + +/** + * Standard error handler for Hono routes + * Handles Zod validation errors, HTTP exceptions, and unexpected errors + * Returns complete Hono response with detailed error information for easier debugging + * + * @param error - The error to handle + * @param c - Hono context + * @param customMessage - Optional custom message to use instead of default error message + */ +export const standardErrorHandler = ( + error: Error | z.ZodError | HTTPException | unknown, + c: Context, + customMessage?: string +) => { + // Handle Zod validation errors with detailed information + if (error instanceof z.ZodError) { + return c.json(handleZodError(error), 400); + } + + // Handle HTTP exceptions - let them manage their own response + if (error instanceof HTTPException) { + return error.getResponse(); + } + + // Log unexpected errors but still return helpful details + console.error('Unhandled error:', error); + + return c.json( + { + error: 'Internal Server Error', + message: customMessage || (error instanceof Error ? error.message : 'Unknown error'), + }, + 500 + ); +};