Update documentation for future background agents

This commit is contained in:
Nate Kelley 2025-07-17 12:34:48 -06:00
parent 7b24d167b9
commit e44cda8f10
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
4 changed files with 488 additions and 38 deletions

View File

@ -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<typeof UpdateOrganizationRequestSchema>;
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<ResponseType> {
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

View File

@ -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<typeof UpdateOrganizationRequestSchema>;
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<ResponseType> {
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

View File

@ -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;

View File

@ -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
);
};