mirror of https://github.com/buster-so/buster.git
|
||
---|---|---|
.. | ||
.cursor | ||
scripts | ||
src | ||
.dockerignore | ||
.env.example | ||
.gitignore | ||
CLAUDE.md | ||
Dockerfile | ||
Dockerfile.prebuilt | ||
README.md | ||
biome.json | ||
env.d.ts | ||
package.json | ||
test-docker.sh | ||
tsconfig.json | ||
tsup.config.ts | ||
turbo.json | ||
vitest.config.ts |
README.md
Server Application
This is the main TypeScript/Node.js API server using Hono framework. It assembles packages to create the REST API.
Installation
pnpm add @buster-app/server
Overview
@buster-app/server
is responsible for:
- HTTP API endpoints
- Request/response handling
- Authentication middleware
- Routing and middleware
- Assembling package functionality
Technology Stack
- Runtime: Bun (optimized for performance)
- Framework: Hono (lightweight, fast)
- Validation: Zod with @hono/zod-validator
- Architecture: File-path based routing
Architecture
Packages → @buster-app/server → HTTP API
↓
File-based routes
(Matches URL paths)
File-Path Based Routing
CRITICAL: Routes are organized using file-path based structure that matches the actual API path.
Routing Structure
server/
├── src/
│ ├── api/
│ │ ├── v2/
│ │ │ ├── users/
│ │ │ │ ├── index.ts # /v2/users
│ │ │ │ ├── GET.ts # GET /v2/users
│ │ │ │ ├── POST.ts # POST /v2/users
│ │ │ │ └── [id]/
│ │ │ │ ├── index.ts # /v2/users/:id
│ │ │ │ ├── GET.ts # GET /v2/users/:id
│ │ │ │ ├── PUT.ts # PUT /v2/users/:id
│ │ │ │ ├── DELETE.ts # DELETE /v2/users/:id
│ │ │ │ └── suggested-questions/
│ │ │ │ ├── index.ts # /v2/users/:id/suggested-questions
│ │ │ │ └── GET.ts # GET /v2/users/:id/suggested-questions
│ │ │ ├── organizations/
│ │ │ │ ├── index.ts
│ │ │ │ ├── GET.ts
│ │ │ │ ├── POST.ts
│ │ │ │ └── [orgId]/
│ │ │ │ ├── members/
│ │ │ │ │ ├── GET.ts
│ │ │ │ │ └── POST.ts
│ │ │ │ └── settings/
│ │ │ │ └── GET.ts
│ │ │ └── index.ts # Main v2 router
│ │ └── index.ts # Main API router
│ └── index.ts # App entry point
Route File Pattern
Each HTTP method gets its own file:
// src/api/v2/users/[id]/GET.ts
import { Hono } from 'hono';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
import type { GetUserResponse } from '@buster/server-shared';
import { getUserHandler } from './handlers/get-user';
const ParamsSchema = z.object({
id: z.string().uuid()
});
export const GET = new Hono()
.get(
'/',
zValidator('param', ParamsSchema),
async (c) => {
const user = c.get('busterUser');
const params = c.req.valid('param');
const response = await getUserHandler({
userId: params.id,
requestingUser: user
});
return c.json<GetUserResponse>(response);
}
);
Index Router Pattern
Each directory has an index.ts that combines routes:
// src/api/v2/users/[id]/index.ts
import { Hono } from 'hono';
import { GET } from './GET';
import { PUT } from './PUT';
import { DELETE } from './DELETE';
import { suggestedQuestions } from './suggested-questions';
export const userById = new Hono()
.route('/', GET)
.route('/', PUT)
.route('/', DELETE)
.route('/suggested-questions', suggestedQuestions);
Parent Router Pattern
// src/api/v2/users/index.ts
import { Hono } from 'hono';
import { GET } from './GET';
import { POST } from './POST';
import { userById } from './[id]';
export const users = new Hono()
.route('/', GET)
.route('/', POST)
.route('/:id', userById);
Handler Pattern
Handlers are pure functions in separate files:
// src/api/v2/users/[id]/handlers/get-user.ts
import { z } from 'zod';
import type { User } from '@buster/database';
import { getUser } from '@buster/database';
import { checkPermission } from '@buster/access-controls';
const GetUserHandlerParamsSchema = z.object({
userId: z.string().uuid(),
requestingUser: z.custom<User>()
});
type GetUserHandlerParams = z.infer<typeof GetUserHandlerParamsSchema>;
export async function getUserHandler(params: GetUserHandlerParams) {
const validated = GetUserHandlerParamsSchema.parse(params);
// Check permissions
const canAccess = await checkPermission({
user: validated.requestingUser,
action: 'read',
resource: {
type: 'user',
id: validated.userId
}
});
if (!canAccess) {
throw new ForbiddenError();
}
// Get user from database
const user = await getUser({ userId: validated.userId });
if (!user) {
throw new NotFoundError('User not found');
}
return {
user,
permissions: await getUserPermissions(user.id)
};
}
Middleware
Authentication Middleware
// src/middleware/auth.ts
import type { Context, Next } from 'hono';
import { validateSession } from '@buster/access-controls';
export async function requireAuth(c: Context, next: Next) {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return c.json({ error: 'Unauthorized' }, 401);
}
const user = await validateSession(token);
if (!user) {
return c.json({ error: 'Invalid session' }, 401);
}
c.set('busterUser', user);
await next();
}
Error Handling Middleware
// src/middleware/error-handler.ts
export async function errorHandler(c: Context, next: Next) {
try {
await next();
} catch (error) {
if (error instanceof ValidationError) {
return c.json({
error: 'Validation failed',
details: error.errors
}, 400);
}
if (error instanceof ForbiddenError) {
return c.json({ error: 'Forbidden' }, 403);
}
if (error instanceof NotFoundError) {
return c.json({ error: error.message }, 404);
}
// Log internal errors
console.error('Unhandled error:', error);
// Don't expose internal errors
return c.json({ error: 'Internal server error' }, 500);
}
}
Type Safety
Request/Response Types
All types come from server-shared:
import type {
CreateUserRequest,
CreateUserResponse,
GetUsersRequest,
GetUsersResponse
} from '@buster/server-shared';
// Never define types locally
// ❌ Wrong
interface LocalUserType {
id: string;
email: string;
}
// ✅ Correct - import from server-shared
import type { User } from '@buster/server-shared';
Validation Pattern
import { zValidator } from '@hono/zod-validator';
import { CreateUserRequestSchema } from '@buster/server-shared';
// Validate request body
.post(
'/',
zValidator('json', CreateUserRequestSchema),
async (c) => {
const data = c.req.valid('json');
// data is fully typed
}
)
// Validate query params
.get(
'/',
zValidator('query', GetUsersQuerySchema),
async (c) => {
const query = c.req.valid('query');
// query is fully typed
}
)
App Structure
Main App Setup
// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { api } from './api';
import { errorHandler } from './middleware/error-handler';
const app = new Hono();
// Global middleware
app.use('*', cors());
app.use('*', logger());
app.use('*', errorHandler);
// Mount API routes
app.route('/api', api);
// Health check
app.get('/health', (c) => c.json({ status: 'ok' }));
export default app;
Environment Configuration
// src/config/env.ts
import { z } from 'zod';
const EnvSchema = z.object({
PORT: z.string().default('8080'),
DATABASE_URL: z.string(),
REDIS_URL: z.string(),
JWT_SECRET: z.string(),
NODE_ENV: z.enum(['development', 'production', 'test'])
});
export const env = EnvSchema.parse(process.env);
Testing Patterns
Route Testing
describe('GET /v2/users/:id', () => {
it('should return user when authorized', async () => {
const app = createTestApp();
const response = await app.request('/api/v2/users/123', {
headers: {
Authorization: 'Bearer valid-token'
}
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.user).toBeDefined();
});
it('should return 401 when not authenticated', async () => {
const app = createTestApp();
const response = await app.request('/api/v2/users/123');
expect(response.status).toBe(401);
});
});
Handler Testing
describe('getUserHandler', () => {
it('should return user data', async () => {
const mockUser = { id: '123', email: 'test@example.com' };
jest.spyOn(database, 'getUser').mockResolvedValue(mockUser);
const result = await getUserHandler({
userId: '123',
requestingUser: mockUser
});
expect(result.user).toEqual(mockUser);
});
});
Best Practices
DO:
- Use file-path based routing
- Separate handlers from routes
- Import types from server-shared
- Validate all inputs with Zod
- Use functional handlers
- Apply auth middleware globally
- Handle errors gracefully
- Test routes and handlers separately
DON'T:
- Define types locally
- Mix business logic with routes
- Use classes for handlers
- Skip validation
- Expose internal errors
- Access database directly
- Hardcode configuration
WebSocket Support
WebSocket Handler
// src/api/ws/chat.ts
import { createBunWebSocket } from 'hono/bun';
import type { ServerWebSocket } from 'bun';
const { upgradeWebSocket, websocket } = createBunWebSocket<{ user: User }>();
export const chatWebSocket = new Hono()
.get(
'/chat',
upgradeWebSocket((c) => {
return {
onOpen(evt, ws) {
console.info('WebSocket opened');
},
onMessage(evt, ws) {
const message = JSON.parse(evt.data);
// Handle message
ws.send(JSON.stringify({ type: 'ack' }));
},
onClose(evt, ws) {
console.info('WebSocket closed');
}
};
})
);
Performance Optimization
Response Caching
import { cache } from 'hono/cache';
// Cache GET requests
.get(
'/',
cache({
cacheName: 'users',
cacheControl: 'max-age=3600'
}),
getUsersHandler
)
Rate Limiting
import { rateLimiter } from './middleware/rate-limiter';
// Apply rate limiting
.use('/api/*', rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
}))
Development
# Development
turbo dev --filter=@buster-app/server
# Build
turbo build --filter=@buster-app/server
# Test
turbo test:unit --filter=@buster-app/server
turbo test:integration --filter=@buster-app/server
# Lint
turbo lint --filter=@buster-app/server
Deployment
The server runs on Bun runtime for optimal performance:
# Production
turbo build --filter=@buster-app/server
bun run dist/index.js
This app should ONLY assemble packages and handle HTTP concerns. All business logic belongs in packages.