Entity System
If you followed the Quick Start or Data and Entities, you already have an entity generating routes. This section goes deeper into everything the entity system can do.
What you will learn
Section titled “What you will learn”- How to define fields, indexes, timestamps, and relations
- How to add named operations like lookups, transitions, and search
- How to control auth, permissions, and rate limits per generated route
- How to swap storage backends without changing your entity code
- How to override or extend generated routes when the default behavior is not enough
The progression
Section titled “The progression”Most entities start simple and grow. Here is what that looks like:
1. Start with a model
Section titled “1. Start with a model”import { defineEntity, field } from '@lastshotlabs/slingshot';
const Post = defineEntity('Post', { namespace: 'blog', fields: { id: field.string({ primary: true, default: 'uuid' }), title: field.string(), body: field.string(), createdAt: field.date({ default: 'now' }), },});You now have GET /posts, POST /posts, PATCH /posts/:id, DELETE /posts/:id with
validation and OpenAPI docs.
2. Add domain behavior
Section titled “2. Add domain behavior”Your blog needs “publish” and “lookup by author.” These are operations:
// @skip-typecheckimport { defineOperations, op } from '@lastshotlabs/slingshot';
const PostOps = defineOperations(Post, { byAuthor: op.lookup({ fields: { authorId: 'param:authorId' }, returns: 'many', }), publish: op.transition({ field: 'status', from: 'draft', to: 'published', match: { id: 'param:id' }, }),});Now you also have GET /posts/by-author/:authorId and POST /posts/:id/publish.
3. Add auth and permissions
Section titled “3. Add auth and permissions”Readers can browse. Only authenticated users can create. Only the author can edit:
const Post = defineEntity('Post', { namespace: 'blog', fields: { id: field.string({ primary: true, default: 'uuid' }), title: field.string(), body: field.string(), authorId: field.string(), status: field.enum(['draft', 'published'] as const, { default: 'draft' }), createdAt: field.date({ default: 'now' }), }, routes: { defaults: { auth: 'userAuth' }, list: { auth: 'none' }, get: { auth: 'none' }, update: { permission: { requires: 'blog:post.write', ownerField: 'authorId' }, }, },});4. Swap storage
Section titled “4. Swap storage”Move from in-memory to a real database with one change:
// @skip-typecheckentity({ config: Post, operations: PostOps, adapter: 'postgres' });Your entity definition, operations, and route policy stay the same.
Read in order
Section titled “Read in order” defineEntity Fields, indexes, timestamps, relations, system fields, and naming conventions.
Operations Lookups, transitions, field updates, search, aggregates, and batches.
Route Policy Auth, permissions, middleware, rate limits, and event publication per generated route.
Storage and Adapter Wiring Memory, SQLite, Postgres, MongoDB, Redis — and how to swap between them.
Generated Routes, Overrides, and Extra Routes Customize one generated route, add your own, or break out of the entity shell.
Common patterns
Section titled “Common patterns”| You want to… | See |
|---|---|
| Look up records by a field | Operations: lookups |
| Add a status transition (draft → published) | Operations: transitions |
| Require login on some routes but not others | Route Policy |
| Let users only edit their own records | Route Policy: ownerField |
| Use Postgres instead of in-memory | Storage and Adapter Wiring |
| Override a single generated route | Route overrides |
| Add a route that is not generated by the entity | Extra routes |
| Drop out of the entity system entirely | Lower-level Seams |