backend-route-creation
0
总安装量
6
周安装量
安装命令
npx skills add https://github.com/workshop-ventures/skills --skill backend-route-creation
Agent 安装分布
claude-code
5
opencode
3
codex
3
windsurf
2
antigravity
2
Skill 文档
Backend Route Creation
This skill creates new API routes using koa-zod-router with Zod validation schemas following established patterns.
Overview
Routes use schemas defined in @{project}/types for validation. This ensures type safety between backend and frontend.
Route File Structure
Routes are organized by resource in apps/backend/src/routes/:
apps/backend/src/routes/
âââ workflows.ts # /api/workflows endpoints
âââ workflow-runs.ts # /api/workflow-runs endpoints
âââ {resource}.ts # New resource routes
Step 1: Define API Schemas in @{project}/types
First, create schemas in libs/types/src/api/{resource}.ts:
// libs/types/src/api/{resource}.ts
import { z } from 'zod/v3';
import { ResourceStatusOptions } from '../lib/Resource'; // Import enum options if any
import {
paginationQuerySchema,
idParamSchema,
listResponseSchema,
singleResponseSchema,
messageResponseSchema,
} from './common';
// ============================================
// Resource Entity Schema (for responses)
// ============================================
export const resourceSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
status: z.enum(ResourceStatusOptions),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
});
// ============================================
// GET /api/resources - List Resources
// ============================================
export const listResourcesQuerySchema = paginationQuerySchema.extend({
status: z.enum(ResourceStatusOptions).optional(),
search: z.string().optional(),
});
export const listResourcesResponseSchema = listResponseSchema(resourceSchema);
// Use z.input for query types (parameters with defaults should be optional)
export type ListResourcesQuery = z.input<typeof listResourcesQuerySchema>;
export type ListResourcesResponse = z.infer<typeof listResourcesResponseSchema>;
// ============================================
// GET /api/resources/:id - Get Resource
// ============================================
export const getResourceParamsSchema = idParamSchema;
export const getResourceResponseSchema = singleResponseSchema(resourceSchema);
export type GetResourceParams = z.infer<typeof getResourceParamsSchema>;
export type GetResourceResponse = z.infer<typeof getResourceResponseSchema>;
// ============================================
// POST /api/resources - Create Resource
// ============================================
export const createResourceBodySchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
// Add required fields for creation
});
export const createResourceResponseSchema = singleResponseSchema(resourceSchema);
export type CreateResourceBody = z.infer<typeof createResourceBodySchema>;
export type CreateResourceResponse = z.infer<typeof createResourceResponseSchema>;
// ============================================
// PUT /api/resources/:id - Update Resource
// ============================================
export const updateResourceParamsSchema = idParamSchema;
export const updateResourceBodySchema = z.object({
name: z.string().min(1).optional(),
description: z.string().optional(),
// All fields optional for partial updates
});
export const updateResourceResponseSchema = singleResponseSchema(resourceSchema);
export type UpdateResourceParams = z.infer<typeof updateResourceParamsSchema>;
export type UpdateResourceBody = z.infer<typeof updateResourceBodySchema>;
export type UpdateResourceResponse = z.infer<typeof updateResourceResponseSchema>;
// ============================================
// DELETE /api/resources/:id - Delete Resource
// ============================================
export const deleteResourceParamsSchema = idParamSchema;
export const deleteResourceResponseSchema = messageResponseSchema;
export type DeleteResourceParams = z.infer<typeof deleteResourceParamsSchema>;
export type DeleteResourceResponse = z.infer<typeof deleteResourceResponseSchema>;
Then export from libs/types/src/api/index.ts:
export * from './{resource}';
Step 2: Create the Route File
Create apps/backend/src/routes/{resource}.ts:
import zodRouter from 'koa-zod-router';
import Resource from '../models/Resource';
import {
// Query/Params schemas
listResourcesQuerySchema,
getResourceParamsSchema,
updateResourceParamsSchema,
deleteResourceParamsSchema,
// Body schemas
createResourceBodySchema,
updateResourceBodySchema,
} from '@{project}/types';
const router = zodRouter();
// GET /api/resources - List all
router.register({
method: 'get',
path: '/',
validate: {
query: listResourcesQuerySchema,
},
handler: async (ctx) => {
const { skip, limit, status, search } = ctx.request.query;
const query: Record<string, unknown> = {};
if (status) query.status = status;
if (search) query.name = { $regex: search, $options: 'i' };
const results = await Resource.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
const total = await Resource.countDocuments(query);
ctx.status = 200;
ctx.body = { total, results };
},
});
// GET /api/resources/:id - Get by ID
router.register({
method: 'get',
path: '/:id',
validate: {
params: getResourceParamsSchema,
},
handler: async (ctx) => {
const { id } = ctx.request.params;
const result = await Resource.findOne({ id });
if (!result) {
ctx.status = 404;
ctx.body = { message: 'Resource not found' };
return;
}
ctx.status = 200;
ctx.body = { result };
},
});
// POST /api/resources - Create
router.register({
method: 'post',
path: '/',
validate: {
body: createResourceBodySchema,
},
handler: async (ctx) => {
const body = ctx.request.body;
const result = new Resource(body);
await result.save();
ctx.status = 201;
ctx.body = { result };
},
});
// PUT /api/resources/:id - Update
router.register({
method: 'put',
path: '/:id',
validate: {
params: updateResourceParamsSchema,
body: updateResourceBodySchema,
},
handler: async (ctx) => {
const { id } = ctx.request.params;
const updates = ctx.request.body;
const result = await Resource.findOne({ id });
if (!result) {
ctx.status = 404;
ctx.body = { message: 'Resource not found' };
return;
}
Object.assign(result, updates);
await result.save();
ctx.status = 200;
ctx.body = { result };
},
});
// DELETE /api/resources/:id - Delete
router.register({
method: 'delete',
path: '/:id',
validate: {
params: deleteResourceParamsSchema,
},
handler: async (ctx) => {
const { id } = ctx.request.params;
const result = await Resource.findOne({ id });
if (!result) {
ctx.status = 404;
ctx.body = { message: 'Resource not found' };
return;
}
await Resource.deleteOne({ id });
ctx.status = 200;
ctx.body = { message: 'Resource deleted' };
},
});
export default router;
Step 3: Mount the Route in main.ts
Add to apps/backend/src/main.ts:
import resourceRoutes from './routes/{resource}';
// ... existing middleware ...
// Mount routes
app.use(mount('/api/{resource}', resourceRoutes.routes()));
Zod Schema Patterns
Query Parameters (in @{project}/types)
Use z.coerce for converting string query params:
export const listQuerySchema = paginationQuerySchema.extend({
status: z.enum(['active', 'inactive']).optional(),
includeArchived: z.coerce.boolean().optional().default(false),
search: z.string().optional(),
});
// IMPORTANT: Use z.input for query types so defaults remain optional
export type ListQuery = z.input<typeof listQuerySchema>;
URL Parameters
export const resourceParamsSchema = z.object({
id: z.string(),
// For numeric IDs: userId: z.coerce.number(),
});
Request Body
// Create schema - required fields
export const createBodySchema = z.object({
name: z.string().min(1),
email: z.string().email(),
roles: z.array(z.enum(['admin', 'user', 'guest'])),
metadata: z.object({
source: z.string().optional(),
tags: z.array(z.string()).optional(),
}).optional(),
});
// Update schema - all fields optional
export const updateBodySchema = z.object({
name: z.string().min(1).optional(),
email: z.string().email().optional(),
roles: z.array(z.enum(['admin', 'user', 'guest'])).optional(),
});
Using Shared Type Options
Import enum options from @{project}/types:
import { StatusOptions, RoleOptions } from '@{project}/types';
export const schema = z.object({
status: z.enum(StatusOptions),
role: z.enum(RoleOptions),
});
Route Handler Patterns
Standard List Response
ctx.status = 200;
ctx.body = {
total: count,
results: items,
};
Standard Single Response
ctx.status = 200;
ctx.body = { result: item };
Standard Create Response
ctx.status = 201;
ctx.body = { result: newItem };
Standard Error Responses
// Not found
ctx.status = 404;
ctx.body = { message: 'Resource not found' };
return;
// Bad request
ctx.status = 400;
ctx.body = { message: 'Invalid input', details: '...' };
return;
// Forbidden
ctx.status = 403;
ctx.body = { message: 'Access denied' };
return;
Filtering in List Endpoints
handler: async (ctx) => {
const { skip, limit, status, search } = ctx.request.query;
const query: Record<string, unknown> = {};
if (status) {
query.status = status;
}
if (search) {
query.name = { $regex: search, $options: 'i' };
}
const results = await Model.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
const total = await Model.countDocuments(query);
ctx.status = 200;
ctx.body = { total, results };
}
Action Routes
For non-CRUD actions like /api/workflows/:id/publish:
// In @{project}/types: define the schema
export const publishResourceParamsSchema = idParamSchema;
export const publishResourceResponseSchema = singleResponseSchema(resourceSchema);
// In route file:
router.register({
method: 'post',
path: '/:id/publish',
validate: {
params: publishResourceParamsSchema,
},
handler: async (ctx) => {
const { id } = ctx.request.params;
const resource = await Resource.findOne({ id });
if (!resource) {
ctx.status = 404;
ctx.body = { message: 'Resource not found' };
return;
}
if (resource.status === 'published') {
ctx.status = 400;
ctx.body = { message: 'Resource is already published' };
return;
}
resource.status = 'published';
resource.publishedAt = new Date();
await resource.save();
ctx.status = 200;
ctx.body = { result: resource };
},
});
Complete Example
See existing implementation:
- Schemas:
libs/types/src/api/workflows.ts - Routes:
apps/backend/src/routes/workflows.ts
Checklist
After creating a new route:
- Create API schemas in
libs/types/src/api/{resource}.ts - Export schemas from
libs/types/src/api/index.ts - Build types library:
npx tsc -b libs/types/tsconfig.lib.json - Create the route file in
apps/backend/src/routes/ - Import and mount in
main.tswithapp.use(mount('/api/{path}', routes.routes())) - Test the endpoints with curl or your API client
- Create corresponding frontend API module in
apps/webapp/src/api/ - Create React Query hooks in
apps/webapp/src/hooks/