Generated Routes, Overrides, and Extra Routes
The entity system generates routes by default. When the default behavior is not enough, you have two escape hatches: overrides (replace a generated route) and extra routes (add new ones).
What you get by default
Section titled “What you get by default”An entity with route config generates up to five CRUD routes plus one route per operation:
POST /posts ← createGET /posts ← listGET /posts/:id ← getPATCH /posts/:id ← updateDELETE /posts/:id ← deleteGET /posts/by-author/:authorId ← lookup operationPOST /posts/:id/publish ← transition operationEvery route gets request validation, response schemas, and OpenAPI documentation.
Override a generated route
Section titled “Override a generated route”Use an override when you want to keep the route identity (same method, same path, same OpenAPI entry) but replace the handler logic.
Scenario: The default GET /posts/:id returns the raw record. You want to add a view count:
// @skip-typecheckimport { definePackage, entity } from '@lastshotlabs/slingshot';import { defineEntityExecutor } from '@lastshotlabs/slingshot-entity';import { Post } from '../entities/post';import { PostOps } from '../entities/post-operations';
export const blogPackage = definePackage({ name: 'blog', entities: [ entity({ config: Post, operations: PostOps, overrides: { get: defineEntityExecutor({ summary: 'Get a post and increment view count', build: ({ entityAdapter }) => async ({ params, respond }) => { const post = await entityAdapter.get(params.id); if (!post) return respond.notFound();
// Custom logic: increment view count await entityAdapter.update(params.id, { viewCount: (post.viewCount ?? 0) + 1, });
return respond.json({ ...post, viewCount: (post.viewCount ?? 0) + 1 }); }, }), }, }), ],});What the executor builder receives
Section titled “What the executor builder receives”The build function receives an EntityRouteExecutorBuilderContext:
| Property | What it is |
|---|---|
entity | The resolved entity config |
entityAdapter | The entity’s storage adapter (get, list, create, update, delete + operations) |
routeKey | Which route this is ('get', 'create', etc.) |
getEntityAdapter | Access another entity’s adapter by plugin + entity name |
What the handler receives
Section titled “What the handler receives”The handler function receives the route execution context:
| Property | What it is |
|---|---|
params | URL parameters |
request | The Hono Context object |
entity | Resolved entity config |
entityAdapter | The entity’s storage adapter |
respond | Typed response helpers (json, notFound, noContent, etc.) |
filter | Any filters applied by middleware or route policy |
dataScopeBindings | Resolved data scope bindings |
Override with custom request/response schemas
Section titled “Override with custom request/response schemas”You can also override the OpenAPI schema for the route:
// @skip-typecheckoverrides: { get: defineEntityExecutor({ summary: 'Get a post with related data', request: { query: z.object({ include: z.enum(['author', 'comments']).optional(), }), }, responses: { 200: { description: 'Post with optional related data', schema: z.object({ post: z.any(), author: z.any().optional(), comments: z.array(z.any()).optional(), }), }, }, build: ({ entityAdapter, getEntityAdapter }) => async ({ params, query, respond }) => { const post = await entityAdapter.get(params.id); if (!post) return respond.notFound();
const result: Record<string, unknown> = { post }; if (query?.include === 'author') { const users = getEntityAdapter({ plugin: 'auth', entity: 'User' }); result.author = await users.get(post.authorId); } return respond.json(result); }, }),},Add an extra route
Section titled “Add an extra route”Use extra routes when you want a new endpoint that lives alongside the generated entity routes but is not one of the defaults.
Scenario: Add a POST /posts/:id/preview endpoint that returns a rendered preview:
// @skip-typecheckimport { defineEntityRoute, entity } from '@lastshotlabs/slingshot-entity';
entity({ config: Post, operations: PostOps, extraRoutes: [ defineEntityRoute({ key: 'preview', method: 'post', path: '/:id/preview', auth: 'userAuth', summary: 'Preview a post with rendered markdown', buildExecutor: ({ entityAdapter }) => async ({ params, respond }) => { const post = await entityAdapter.get(params.id); if (!post) return respond.notFound(); return respond.json({ title: post.title, html: `<article><h1>${post.title}</h1>${post.body}</article>`, }); }, }), defineEntityRoute({ key: 'stats', method: 'get', path: '/stats', auth: 'userAuth', permission: { requires: 'blog:post.admin' }, summary: 'Get post statistics', buildExecutor: ({ entityAdapter }) => async ({ respond }) => { const all = await entityAdapter.list({}); const published = all.items.filter((p: any) => p.status === 'published').length; return respond.json({ total: all.items.length, published, draft: all.items.length - published, }); }, }), ],});Extra routes can use the same policy options as generated routes: auth, permission,
rateLimit, middleware, event, and typed request/responses.
Extra route with events
Section titled “Extra route with events”defineEntityRoute({ key: 'feature', method: 'post', path: '/:id/feature', auth: 'userAuth', permission: { requires: 'blog:post.moderate' }, event: { key: 'blog:post.featured', payload: ['id'], exposure: ['client-safe'], }, buildExecutor: ({ entityAdapter }) => async ({ params, respond }) => { await entityAdapter.update(params.id, { featured: true }); return respond.json({ featured: true }); },});Collision rules
Section titled “Collision rules”The framework does not allow ambiguous routes. If you create an extra route with the same method and path as a generated route, the runtime throws an error at boot time.
To replace a generated route, use an override, not an extra route. This makes the intent explicit and avoids silent shadowing.
Disable generated routes
Section titled “Disable generated routes”If you do not want certain CRUD routes at all:
routes: { disable: ['delete'], // no DELETE endpoint},Disabled routes are not registered — they do not appear in OpenAPI docs.
When to use what
Section titled “When to use what”| You want to… | Use |
|---|---|
| Use the default generated behavior | Nothing — it works |
| Change how a generated route works | overrides |
| Add a new route inside the entity shell | extraRoutes |
| Remove a generated route | routes.disable |
| Add a route outside the entity shell | domain() + route.* in the package |
- Config-Driven Domain example — entities, operations, overrides, and extra routes in a complete app
- Package-First Authoring — when the route belongs to the package, not the entity