mirror of https://github.com/buster-so/buster.git
Update documentation for future background agents
This commit is contained in:
parent
7b24d167b9
commit
e44cda8f10
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue