skills/tb-route-builder/SKILL.md
Scaffold a full CRUD route — prompts for resource name, HTTP methods, and access level, then generates backend routes, shared schemas, frontend API functions, react-query hooks, and query keys.
npx skillsauth add TravisBumgarner/claude-brain tb-route-builderInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Repeatable skill for adding a new API resource end-to-end. Prompts the user for details, then scaffolds backend routes, shared Zod schemas, frontend API functions, and react-query hooks.
any: Never destructure req.params, req.query, or req.body directly — always parse with Zod first via .safeParse(), then use the typed .data result.packages/shared. Both backend and frontend import from shared — avoid defining types only in one package.useQuery for reads and useMutation for writes. Plain fetch functions live in src/api/, hooks in src/hooks/..mutate().string (not Date), BigInt becomes string or number, Buffer becomes base64 string, etc. The DB/Drizzle layer uses native types (timestamp, bigint), but route handlers return plain JSON. Frontend parses back to rich types only when needed for display.Ask these questions one at a time — wait for the user's answer before asking the next question.
photo, comment, invite)GET /api/{resources} — list (paginated)GET /api/{resources}/:id — get by IDPOST /api/{resources} — createPATCH /api/{resources}/:id — updateDELETE /api/{resources}/:id — deletePOST /api/{resources}/:id/vote, GET /api/{resources}/:id/stats)
Example answers: "all", "1, 3, 5", "all + POST /:id/vote"member (default) | moderator (>= UserLevel.MODERATOR) | admin (>= UserLevel.ADMIN)| # | Endpoint | Access level | Ownership check | Params |
|----|-----------------------|--------------|-----------------|--------------------------------------------------|
| 1 | GET /api/photos | member | yes (own list) | ?limit (1–100, default 50), ?offset (≥0, default 0) |
| 2 | GET /api/photos/:id | member | yes | :id (uuid) |
| 3 | POST /api/photos | member | n/a (creating) | body (create schema) |
| 4 | PATCH /api/photos/:id | member | yes | :id (uuid), body (update schema) |
| 5 | DELETE /api/photos/:id| admin | no | :id (uuid) |
Number each row so the user can respond like "2: admin, no" to adjust specific rows.
Params are pre-filled with sensible defaults. All params are validated with Zod:
limit: z.coerce.number().int().min(1).max(100).default(50)offset: z.coerce.number().int().min(0).default(0):id: z.string().uuid()id, createdAt, userId if ownership applies) already filled in, plus placeholder rows for the user to specify additional fields. The user can add, remove, or modify any row:
| # | Column | DB type | TS type | Required | Default |
|----|-----------|--------------------------|----------|----------|---------------|
| 1 | id | uuid, primary key | string | yes | defaultRandom |
| 2 | createdAt | timestamp w/ timezone | string | yes | now() |
| 3 | userId | uuid, FK → users.id | string | yes | — |
| 4 | name | varchar(255) | string | yes | — |
| 5 | ? | ? | ? | ? | ? |
Omit row 3 (userId) if no endpoint has an ownership check.packages/shared)packages/shared/src/schemas/{resource}.ts).refine() requiring at least one field){ {resources}: T[], total: number }type {Resource} = z.infer<typeof {resource}Schema>packages/shared/src/schemas/index.tspackages/shared/src/index.tspackages/shared/src/schemas/responses.ts — create once, skip if exists)ErrorCode type — union of string literal error codes (e.g., 'UNAUTHORIZED', 'FORBIDDEN', 'NOT_FOUND', '{RESOURCE}_NOT_FOUND', 'INVALID_UUID', 'INVALID_REQUEST', 'RATE_LIMITED', 'INTERNAL_ERROR')errorMessages map — ErrorCode → user-friendly string for frontend displayApiResponse<T> discriminated union:
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; errorCode: ErrorCode }
packages/shared/src/constants.ts)API_PATHS:
{resources}: '/api/{resources}',
{resource}: (id: string) => `/api/{resources}/${id}`,
packages/api)packages/api/src/routes/shared/responses.ts
sendSuccess<T>(res, data, status?) — { success: true, data }sendError(res, errorCode, status) — { success: false, errorCode }sendNotFound, sendForbidden, sendBadRequest, sendUnauthorized, sendInternalErrorpackages/api/src/routes/shared/auth.ts
requireUserId(req, res) — extracts req.user.userId, sends 401 if missing, returns { userId } or nullpackages/api/src/routes/shared/validation.ts
idParamsSchema — Zod schema for { id: z.string().uuid() }paginationQuerySchema — Zod schema for { limit?: number, offset?: number } with defaults (limit 50/max 100, offset 0)getOwned{Resource}(id, userId, res) — two-step check: queries DB without userId (404 if not found), then checks userId match (403 if not owned). Returns resource or null.packages/api/src/routes/shared/access.ts
requireModerator(req, res) — checks req.user.userLevel >= UserLevel.MODERATOR, sends 403 if not, returns booleanrequireAdmin(req, res) — checks req.user.userLevel >= UserLevel.ADMIN, sends 403 if not, returns booleanpackages/api/src/routes/shared/index.ts
packages/api/src/middleware/rate-limit.ts
standardRateLimit — 100 requests/min per user (default for most routes)strictRateLimit — 30 requests/min per user (for resource-intensive operations like create/upload)(req as AuthenticatedRequest).user?.userId ?? 'unknown'{ success: false, errorCode: 'RATE_LIMITED' } with HTTP 429packages/api/src/routes/{resources}/)Each handler file follows the validate → process → handler pattern:
interface ValidationContext { userId: string; /* ...parsed data */ }
async function validate(req, res): Promise<ValidationContext | null> {
// 1. Extract auth (requireUserId)
// 2. Parse params/query/body with Zod .safeParse() — NEVER destructure req.params directly
// 3. Check access level if applicable (requireModerator/requireAdmin)
// 4. Check ownership if applicable (getOwned{Resource})
// Return typed context or null (error already sent)
}
async function processRequest(req, res, context): Promise<void> {
// Business logic, DB calls, send response via sendSuccess/sendError
}
export async function handler(req, res): Promise<void> {
const context = await validate(req, res)
if (!context) return
await processRequest(req, res, context)
}
For each selected method, create a file. Apply the access level and ownership checks from the user's per-endpoint permissions table:
list.ts — GET list with pagination (limit/offset query params parsed with Zod). If ownership: filter by userId.get.ts — GET by ID, validates UUID via idParamsSchema.safeParse(). If ownership: getOwned{Resource}().create.ts — POST, validates body with shared create schema via .safeParse(). Sets userId if ownership applies.update.ts — PATCH by ID, validates body with shared update schema. If ownership: getOwned{Resource}().delete.ts — DELETE by ID. If ownership: getOwned{Resource}(). Removes from DB.validate() calls requireModerator()/requireAdmin() if that endpoint's access level requires itError handling in handlers:
processRequest in try/catch for operations that can fail (DB, external services)sendInternalError(res) on catch — never expose internal detailsindex.ts — Router registration:
requireAuth, standardRateLimit (or strictRateLimit for create/write-heavy routes)router.delete('/:id', requireAdmin, deleteHandler)){resources}Routerpackages/api/src/index.ts: app.use('/api/{resources}', {resources}Router)packages/api/src/schema.tspackages/api/src/queries/{resources}.ts:
get{Resource}ById(id, userId?) — single resource, optionally filtered by ownerget{Resources}Page(userId, limit, offset) — paginated listget{Resources}Count(userId) — total count for paginationcreate{Resource}(data) — insert and returnupdate{Resource}(id, data) — partial update and returndelete{Resource}(id) — removenpx drizzle-kit generatepackages/web)packages/web/src/api/{resources}.ts)fetch with auth headers (or authenticatedFetch wrapper if it exists)ApiResponse<T>){ limit, offset } paramspackages/web/src/hooks/queries/use{Resource}.ts)Uses react-query (@tanstack/react-query):
use{Resources}(options?) — paginated list query
queryKeys.{resources}(page, limit)use{Resource}(id) — single resource query
queryKeys.{resource}(id)enabled: !!idpackages/web/src/hooks/mutations/use{Action}{Resource}.ts)One file per mutation, named by action. Uses react-query useMutation:
useCreate{Resource} — POST, invalidates list queries on successuseUpdate{Resource} — PATCH, optimistic update on single resource cache, rollback on error, invalidates list on settleduseDelete{Resource} — DELETE, optimistic removal from list cache, invalidates list on success
.mutate()packages/web/src/queryKeys.ts){resources}: (page?, limit?) => ['{resources}', { page, limit }] as const,
{resource}: (id: string) => ['{resources}', id] as const,
Minimal starter pages to verify end-to-end functionality. These are intentionally bare — just enough to confirm the API works and serve as a blank canvas to expand from.
packages/web/src/pages/{Resources}/{Resources}.tsx)use{Resources}() query hook to fetch paginated listTable (or List) with one row per resource showing key fieldsuseCreate{Resource} mutationuseUpdate{Resource} mutation{Resource}FormModal) that handles both create and edit modes based on whether an existing resource is passed inuseDelete{Resource} mutation through ConfirmationModal<Loading /> shared componentpackages/web/src/pages/{Resource}/{Resource}.tsx)/api/{resources}/:id — reads id from URL paramsuse{Resource}(id) query hook to fetch single resourceTypography labels + values){Resource}FormModal pre-filled with current data, wired to useUpdate{Resource} mutationuseDelete{Resource} mutation through ConfirmationModal, navigates back to list on successroutes.ts:
{resources}: { href: () => '/{resources}', label: '{Resources}' },
{resource}: { href: (id: string) => `/{resources}/${id}`, label: '{Resource}' },
Router.tsx with appropriate route guard (MemberRoute, ModeratorRoute, AdminRoute) based on the most permissive access level from the endpoints tablenpx @biomejs/biome check on every new/modified filegetDiagnostics on every new/modified filenpx drizzle-kit generate if schema changednpm run build -w @scope/shareddevelopment
Use this agent when designing visual interfaces, creating design systems, building component libraries, or refining user-facing aesthetics requiring expert visual design, interaction patterns, and accessibility considerations. Specifically: <example> Context: Product team needs a complete design system for a new fintech application with dark mode, multiple device sizes, and strict accessibility requirements. user: "We need to create a comprehensive design system for our fintech app. Include component library, design tokens, typography scale, color palette, interactive patterns, and dark mode support. Must meet WCAG 2.1 AA." assistant: "I'll create a complete design system starting with understanding your brand guidelines and user base through the context-manager. I'll build a modular component library with documented specs, design tokens in multiple formats (CSS, JSON, Figma), responsive patterns across web and mobile, dark mode variants, and comprehensive accessibility annotations. I'll deliver Figma files, design documentation, and developer handoff specifications." <commentary> Use ui-designer when you need to establish or refine a complete design system, including component libraries, design tokens, and comprehensive visual standards. This agent excels at creating reusable design assets and documentation that scale across teams and platforms. </commentary> </example> <example> Context: A frontend team is building a new feature but needs UI design and interaction patterns before development. user: "Design the booking flow for our travel app. Need 3 screen variations, empty/loading/error states, mobile and desktop layouts, and clear interaction patterns for each step." assistant: "I'll design a complete booking flow with wireframes, visual mockups, and interaction specifications. First, let me check the context-manager for your existing design system and style guide. Then I'll create all screen states, document interaction patterns, provide motion design specifications, and ensure alignment with your design language and accessibility standards." <commentary> Invoke ui-designer when your frontend developers need detailed visual designs, interaction specifications, and asset files before implementation. The agent provides a clear handoff package that developers can build from. </commentary> </example> <example> Context: A redesign initiative is underway and you need UI improvements to an existing product. user: "Our dashboard needs a visual refresh. The functionality is fine but the UX feels dated. Improve the visual hierarchy, modernize colors and typography, add micro-interactions, and ensure it works on mobile." assistant: "I'll analyze your current dashboard using the context-manager, identify visual improvement opportunities, redesign layouts for better hierarchy and scannability, update colors and typography to modern standards, add meaningful micro-interactions, and ensure responsive design. I'll provide before/after comparisons, design rationale, and implementation specifications for your developers." <commentary> Use ui-designer for visual refinements, redesigns, and aesthetic improvements to existing interfaces. This agent modernizes dated UIs while respecting existing functionality and providing clear upgrade paths. </commentary> </example>
development
Expert TypeScript developer specializing in advanced type system usage, full-stack development, and build optimization. Masters type-safe patterns for both frontend and backend with emphasis on developer experience and runtime safety.
development
Audit the codebase against all setup skills (00-09). Reports what is done, partial, not started, or N/A for every checklist item with a summary table.
testing
Production hardening — SSR for OG tags, environment configs, Heroku/NFS hosting, CI/CD with GitHub Actions, security headers, rate limiting, PostHog analytics, and DNS setup.