Skip to content

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.

  • 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

Most entities start simple and grow. Here is what that looks like:

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.

Your blog needs “publish” and “lookup by author.” These are operations:

// @skip-typecheck
import { 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.

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' },
},
},
});

Move from in-memory to a real database with one change:

// @skip-typecheck
entity({ config: Post, operations: PostOps, adapter: 'postgres' });

Your entity definition, operations, and route policy stay the same.

You want to…See
Look up records by a fieldOperations: lookups
Add a status transition (draft → published)Operations: transitions
Require login on some routes but not othersRoute Policy
Let users only edit their own recordsRoute Policy: ownerField
Use Postgres instead of in-memoryStorage and Adapter Wiring
Override a single generated routeRoute overrides
Add a route that is not generated by the entityExtra routes
Drop out of the entity system entirelyLower-level Seams
  • Examples — complete apps that use entities with auth, events, and more
  • Guides — production concerns like testing, deployment, and scaling