Skip to content

Data and Entities

You want a backend API for your data. In most frameworks, that means writing route handlers, validation, error handling, and database queries for every resource by hand.

In Slingshot, you define the model once and the framework generates the rest.

app.config.ts
import { defineApp, defineEntity, definePackage, entity, field } from '@lastshotlabs/slingshot';
const Task = defineEntity('Task', {
namespace: 'app',
fields: {
id: field.string({ primary: true, default: 'uuid' }),
title: field.string(),
done: field.boolean({ default: false }),
},
});
const appPackage = definePackage({
name: 'app',
entities: [entity({ config: Task })],
});
export default defineApp({ port: 3000, packages: [appPackage] });

That gives you five working routes:

Terminal window
# Create a task
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{ "title": "Ship v1" }'
# List all tasks
curl http://localhost:3000/tasks
# Get one task
curl http://localhost:3000/tasks/<id>
# Update a task
curl -X PATCH http://localhost:3000/tasks/<id> \
-H "Content-Type: application/json" \
-d '{ "done": true }'
# Delete a task
curl -X DELETE http://localhost:3000/tasks/<id>

Plus full OpenAPI docs at http://localhost:3000/docs.

CRUD is not always enough. A task tracker needs “complete” and “reopen” actions. A blog needs “publish” and “archive”. These are operations:

import { defineEntity, defineOperations, field, op } from '@lastshotlabs/slingshot';
const Task = defineEntity('Task', {
namespace: 'app',
fields: {
id: field.string({ primary: true, default: 'uuid' }),
title: field.string(),
status: field.enum(['todo', 'done'] as const, { default: 'todo' }),
},
});
const TaskOps = defineOperations(Task, {
complete: op.transition({
field: 'status',
from: 'todo',
to: 'done',
match: { id: 'param:id' },
}),
reopen: op.transition({
field: 'status',
from: 'done',
to: 'todo',
match: { id: 'param:id' },
}),
});
Terminal window
# Complete a task
curl -X POST http://localhost:3000/tasks/<id>/complete
# Reopen it
curl -X POST http://localhost:3000/tasks/<id>/reopen

Operations show up in OpenAPI docs and respect the same auth and permission policies as generated CRUD routes.

Add routes.defaults to require authentication on all generated routes:

const Task = defineEntity('Task', {
namespace: 'app',
fields: {
id: field.string({ primary: true, default: 'uuid' }),
title: field.string(),
done: field.boolean({ default: false }),
authorId: field.string(),
},
routes: {
defaults: { auth: 'userAuth' },
},
});

Now every generated route requires a valid session. Anonymous requests get a 401.

Entities use in-memory storage by default — great for development. Switch to a real database with a one-line adapter change:

entity({
config: Task,
operations: TaskOps,
adapter: 'sqlite', // or 'postgres', 'mongodb', 'redis'
});

Your entity definition stays the same. Only the adapter changes. See Storage and Adapter Wiring for the full setup including connection config.

  • GET, POST, PATCH, DELETE CRUD routes
  • Request and response validation from your field definitions
  • OpenAPI generation
  • Route policy hooks for auth, permissions, middleware, and rate limits
  • Adapter-backed storage: memory, SQLite, Postgres, MongoDB, Redis

You can read and write data. The next concern is who is allowed to: