بنقرة واحدة
new-route
Create a new backend route following project conventions
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Create a new backend route following project conventions
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | new-route |
| description | Create a new backend route following project conventions |
You are implementing a new route in the Talo backend. Follow all conventions and patterns exactly as described below.
Choose the correct tier based on what's being built:
| Tier | Prefix | Auth | Config file | Routes dir |
|---|---|---|---|---|
| Protected | / | JWT (JWT_SECRET) | src/config/protected-routes.ts | src/routes/protected/ |
| API | /v1/ | JWT (game.apiSecret) | src/config/api-routes.ts | src/routes/api/ |
| Public | /public/ | None | src/config/public-routes.ts | src/routes/public/ |
If you are unsure, ask the user before proceeding.
Work through these steps in order:
Determine which tier applies and whether a feature directory already exists (e.g., src/routes/api/player/). If adding to an existing feature, read the existing index.ts and relevant files first to understand the current structure.
File placement:
get.ts, post.ts, update.ts, delete.ts)index.tsRoute file structure:
// src/routes/api/my-feature/get.ts
import { apiRoute } from '../../../lib/routing/router.js'
// If using docs AND this is a module-level export, define docs FIRST (before the route)
const docs = {
description: '...',
samples: []
} satisfies RouteDocs
export const getRoute = apiRoute({
method: 'get',
path: '/:id',
docs,
handler: async (ctx) => {
// Use ctx.em for database queries
// Use ctx.state.validated for validated input
return {
status: 200,
body: { ... }
}
}
})
Use the correct route helper and context for the tier:
apiRoute / APIRouteContextprotectedRoute / ProtectedRouteContextpublicRoute / PublicRouteContextAuthorization middleware (import from src/middleware/policy-middleware.ts):
middleware: withMiddleware(
requireScopes([APIKeyScope.READ_PLAYERS]), // API routes: ALWAYS first
loadAlias, // then resource loaders
)
// Protected routes:
middleware: withMiddleware(
ownerGate('view settings'), // or userTypeGate([...]) - ALWAYS first
requireEmailConfirmed, // then email check
loadGame, // then resource loaders
)
Ordering rules:
requireScopes() → resource loadersownerGate() / userTypeGate() → requireEmailConfirmed → resource loaderswithMiddleware() in common.ts[...middleware1, ...middleware2]ownerGate() (not userTypeGate([])) for OWNER-only routesCustom route middleware goes in common.ts:
// src/routes/api/my-feature/common.ts
import { APIRouteContext } from '../../../lib/routing/context.js'
import { Next } from 'koa'
export async function loadMyEntity(ctx: APIRouteContext<{ entity: MyEntity }>, next: Next) {
const entity = await ctx.em.repo(MyEntity).findOne({ id: ctx.params.id })
if (!entity) return ctx.throw(404, 'Entity not found')
ctx.state.entity = entity
await next()
}
Use return ctx.throw() (with return) for type narrowing after the throw.
schema: (z) => ({
// For route params:
params: z.object({
id: z.coerce.number().meta({ description: 'The entity ID' }),
}),
// For query strings:
query: z.object({
page: z.coerce.number().optional(),
}),
// For request body:
body: z.object({
name: z.string({ error: 'name is missing' }).min(1, { message: 'name is invalid' }),
}),
// For headers (use looseObject):
headers: z.looseObject({
'x-talo-alias': z.string({ error: 'x-talo-alias is missing' }),
}),
})
Access validated data via ctx.state.validated.body, .query, .params, .headers - NOT ctx.request.body.
Always wrap inline routes with the route helper when using schema:
route(apiRoute({ schema: ..., handler: ... })) // ✅
route({ schema: ..., handler: ... }) // ❌ loses type inference
index.ts// src/routes/api/my-feature/index.ts
import type Router from 'koa-tree-router'
import { apiRouter } from '../../../lib/routing/router.js'
import { getRoute } from './get.js'
import { postRoute } from './post.js'
export function myFeatureAPIRouter(router: Router) {
apiRouter(
'/v1/my-feature',
({ route }) => {
route(getRoute)
route(postRoute)
},
{ router, docsKey: 'MyFeatureAPI' },
)
}
Add to the appropriate config file if it's a new router:
// src/config/api-routes.ts
import { myFeatureAPIRouter } from '../routes/api/my-feature/index.js'
export function configureAPIRoutes(app: Koa) {
const mainRouter = new Router()
// ... other routers
myFeatureAPIRouter(mainRouter)
app.use(mainRouter.routes())
}
If docs exist for other routes in this folder, add to docs.ts. If this is the first route with docs, create docs.ts:
// src/routes/api/my-feature/docs.ts
import { RouteDocs } from '../../../lib/docs/docs-registry.js'
export const getDocs = {
description: 'Get a my-feature by ID',
samples: [
{
title: 'Get my-feature',
request: {},
response: { status: 200, body: { myFeature: { id: 1 } } },
},
],
} satisfies RouteDocs
Key docs rules:
docs const BEFORE the route (avoid "Cannot access before initialization")docsKey on the router sets the service name for all routesrequireScopes() - do NOT add them manuallyCreate tests in the matching location:
src/routes/api/my-feature/ → tests at tests/routes/api/my-feature-api/src/routes/protected/my-feature/ → tests at tests/routes/protected/my-feature/Follow the pattern of existing test files in the project. Run npm test path/to/test to verify.
[feature]Router or [feature]APIRouterctx.em for all database accessreturn ctx.throw(status, message) for type narrowing