api-route
This skill should be used when the user asks to "create an API route", "add an endpoint", "build a backend route", "create an API endpoint", "add a Hono route", or mentions creating REST endpoints, API handlers, or backend routes. Provides Hono API patterns for CoRATES workers.
When & Why to Use This Skill
This Claude skill accelerates backend development by providing standardized Hono API patterns specifically optimized for CoRATES workers. It streamlines the creation of secure, scalable RESTful endpoints by automating middleware composition, Zod-based request validation, and Drizzle ORM integration, ensuring consistent architecture across the codebase.
Use Cases
- Rapidly scaffolding new REST API endpoints with pre-configured authentication and organization-level permission middleware.
- Implementing robust data validation layers using Zod schemas to ensure type safety and consistent error reporting for incoming requests.
- Streamlining database interactions by generating Drizzle ORM query patterns for CRUD operations and complex joins within Hono routes.
- Standardizing backend error handling by utilizing domain-specific error patterns and centralized status code management.
- Setting up complex middleware chains for feature entitlements, quota management, and context-aware data fetching.
| name | API Route |
|---|---|
| description | This skill should be used when the user asks to "create an API route", "add an endpoint", "build a backend route", "create an API endpoint", "add a Hono route", or mentions creating REST endpoints, API handlers, or backend routes. Provides Hono API patterns for CoRATES workers. |
API Route Creation
Create Hono API routes following CoRATES workers patterns.
Core Principles
- Middleware composition - Chain auth, permissions, validation middleware
- Domain errors - Use createDomainError from @corates/shared
- Zod validation - Define schemas centrally in config/validation.js
- Drizzle ORM - All database operations through Drizzle
- Context isolation - Attach data to Hono context, read via getters
Quick Reference
File Location
packages/workers/src/
routes/
[feature].js # Standalone routes
[feature]/
index.js # Route composition
[subroute].js # Nested routes
config/
validation.js # Zod schemas
middleware/
auth.js # Authentication
requireOrg.js # Org membership
Basic Route Template
import { Hono } from 'hono';
import { eq, and } from 'drizzle-orm';
import { createDb } from '@/db/client.js';
import { items } from '@/db/schema.js';
import { requireAuth, getAuth } from '@/middleware/auth.js';
import { validateRequest } from '@/config/validation.js';
import { createDomainError, ITEM_ERRORS, SYSTEM_ERRORS } from '@corates/shared';
import { itemSchemas } from '@/config/validation.js';
const itemRoutes = new Hono();
// Apply auth to all routes
itemRoutes.use('*', requireAuth);
// GET - List items
itemRoutes.get('/', async c => {
const { user } = getAuth(c);
const db = createDb(c.env.DB);
try {
const results = await db.select().from(items).where(eq(items.userId, user.id));
return c.json(results);
} catch (error) {
console.error('Error fetching items:', error);
const dbError = createDomainError(SYSTEM_ERRORS.DB_ERROR, {
operation: 'fetch_items',
});
return c.json(dbError, dbError.statusCode);
}
});
// POST - Create item
itemRoutes.post('/', validateRequest(itemSchemas.create), async c => {
const { user } = getAuth(c);
const { name, description } = c.get('validatedBody');
const db = createDb(c.env.DB);
try {
const id = crypto.randomUUID();
const now = new Date();
await db.insert(items).values({
id,
name,
description,
userId: user.id,
createdAt: now,
});
return c.json({ id, name, description }, 201);
} catch (error) {
console.error('Error creating item:', error);
const dbError = createDomainError(SYSTEM_ERRORS.DB_ERROR, {
operation: 'create_item',
});
return c.json(dbError, dbError.statusCode);
}
});
export { itemRoutes };
Middleware Chain
Apply middleware in order of specificity:
import { requireAuth, getAuth } from '@/middleware/auth.js';
import { requireOrgMembership, getOrgContext } from '@/middleware/requireOrg.js';
import { requireOrgWriteAccess } from '@/middleware/requireOrgWriteAccess.js';
import { requireEntitlement } from '@/middleware/requireEntitlement.js';
import { requireQuota } from '@/middleware/requireQuota.js';
import { validateRequest } from '@/config/validation.js';
// Full middleware chain for protected route
routes.post(
'/',
requireOrgMembership(), // Check org membership
requireOrgWriteAccess(), // Check write permission
requireEntitlement('feature.x'), // Check feature access
requireQuota('items.max', getItemCount, 1), // Check quota
validateRequest(schema), // Validate body
async c => {
const { user } = getAuth(c);
const { orgId } = getOrgContext(c);
const data = c.get('validatedBody');
// Handler code...
},
);
Context Getters
// Authentication
const { user, session } = getAuth(c);
// Organization context (after requireOrgMembership)
const { orgId, orgRole, org } = getOrgContext(c);
// Validated request body (after validateRequest)
const data = c.get('validatedBody');
// Billing/entitlements (after requireEntitlement)
const entitlements = c.get('entitlements');
const quotas = c.get('quotas');
Zod Validation
Define Schemas
Add to packages/workers/src/config/validation.js:
export const itemSchemas = {
create: z.object({
name: z
.string()
.min(1, 'Name is required')
.max(255)
.transform(val => val.trim()),
description: z
.string()
.max(2000)
.optional()
.transform(val => val?.trim() || null),
}),
update: z.object({
name: z.string().min(1).max(255).optional(),
description: z.string().max(2000).optional(),
}),
};
Common Patterns
// Required string with trim
name: z.string().min(1).max(255).transform(val => val.trim()),
// Optional with null fallback
description: z.string().max(2000).optional().transform(val => val?.trim() || null),
// Email
email: z.string().email('Invalid email'),
// UUID
id: z.string().uuid(),
// Enum
role: z.enum(['owner', 'admin', 'member', 'viewer']),
// Boolean with default
active: z.boolean().optional().default(true),
Error Handling
Domain Errors
import { createDomainError, PROJECT_ERRORS, AUTH_ERRORS, SYSTEM_ERRORS } from '@corates/shared';
// Not found
const error = createDomainError(PROJECT_ERRORS.NOT_FOUND, { projectId });
return c.json(error, error.statusCode);
// Forbidden with reason
const error = createDomainError(AUTH_ERRORS.FORBIDDEN, {
reason: 'insufficient_role',
required: 'admin',
});
return c.json(error, error.statusCode);
// Database error
const dbError = createDomainError(SYSTEM_ERRORS.DB_ERROR, {
operation: 'create_item',
originalError: error.message,
});
return c.json(dbError, dbError.statusCode);
Try-Catch Pattern
routes.get('/:id', async c => {
const { user } = getAuth(c);
const id = c.req.param('id');
const db = createDb(c.env.DB);
try {
const result = await db
.select()
.from(items)
.where(and(eq(items.id, id), eq(items.userId, user.id)))
.get();
if (!result) {
const error = createDomainError(ITEM_ERRORS.NOT_FOUND, { id });
return c.json(error, error.statusCode);
}
return c.json(result);
} catch (error) {
console.error('Error fetching item:', error);
const dbError = createDomainError(SYSTEM_ERRORS.DB_ERROR, {
operation: 'fetch_item',
});
return c.json(dbError, dbError.statusCode);
}
});
Database Operations
Query Patterns
import { createDb } from '@/db/client.js';
import { eq, and, or, like, desc, sql } from 'drizzle-orm';
const db = createDb(c.env.DB);
// Select with join
const results = await db
.select({
id: projects.id,
name: projects.name,
role: projectMembers.role,
})
.from(projects)
.innerJoin(projectMembers, eq(projects.id, projectMembers.projectId))
.where(eq(projectMembers.userId, user.id))
.orderBy(desc(projects.updatedAt));
// Single record
const item = await db
.select()
.from(items)
.where(eq(items.id, id))
.get();
// Insert
await db.insert(items).values({ id, name, createdAt: now });
// Update
await db.update(items)
.set({ name, updatedAt: now })
.where(eq(items.id, id));
// Delete
await db.delete(items).where(eq(items.id, id));
// Batch for atomic operations
await db.batch([
db.insert(projects).values({ ... }),
db.insert(projectMembers).values({ ... }),
]);
Response Patterns
// Success with data
return c.json(result);
// Created (201)
return c.json(newItem, 201);
// Success flag
return c.json({ success: true, id: itemId });
// Array response
return c.json(results);
// Error response
return c.json(error, error.statusCode);
Route Registration
Mount in Main App
// packages/workers/src/index.js
import { itemRoutes } from './routes/items.js';
app.route('/api/items', itemRoutes);
Nested Routes
// In parent route file
import { subRoutes } from './subroute.js';
parentRoutes.route('/:parentId/children', subRoutes);
// Creates: /api/parent/:parentId/children/...
Creation Checklist
When creating an API route:
- Create route file in
packages/workers/src/routes/ - Define Zod schemas in
config/validation.js - Apply appropriate middleware chain
- Use context getters for auth/org/validated data
- Use Drizzle for all database operations
- Return domain errors with proper status codes
- Register route in
index.js
Additional Resources
Reference Files
For detailed patterns:
references/patterns.md- Middleware details, complex queries, nested routesreferences/examples.md- Real route examples from the codebase
Example Files
Working templates in examples/:
ExampleRoutes.js- Complete CRUD route template