api-route

InfinityBowman's avatarfrom InfinityBowman

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.

0stars🔀0forks📁View on GitHub🕐Updated Jan 10, 2026

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.
nameAPI Route
descriptionThis 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

  1. Middleware composition - Chain auth, permissions, validation middleware
  2. Domain errors - Use createDomainError from @corates/shared
  3. Zod validation - Define schemas centrally in config/validation.js
  4. Drizzle ORM - All database operations through Drizzle
  5. 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:

  1. Create route file in packages/workers/src/routes/
  2. Define Zod schemas in config/validation.js
  3. Apply appropriate middleware chain
  4. Use context getters for auth/org/validated data
  5. Use Drizzle for all database operations
  6. Return domain errors with proper status codes
  7. Register route in index.js

Additional Resources

Reference Files

For detailed patterns:

  • references/patterns.md - Middleware details, complex queries, nested routes
  • references/examples.md - Real route examples from the codebase

Example Files

Working templates in examples/:

  • ExampleRoutes.js - Complete CRUD route template