definePackage
A package is a self-contained feature module. It owns its entities, routes, middleware, and mount path. The app root composes packages — it does not implement features.
A blog package
Section titled “A blog package”import { z } from 'zod';import { defineEntity, defineOperations, definePackage, domain, entity, field, index, op, route,} from '@lastshotlabs/slingshot';
// --- Entity ---
const Post = defineEntity('Post', { namespace: 'blog', fields: { id: field.string({ primary: true, default: 'uuid' }), slug: field.string({ immutable: true }), authorId: field.string(), title: field.string(), body: field.string(), status: field.enum(['draft', 'published'] as const, { default: 'draft' }), createdAt: field.date({ default: 'now' }), }, indexes: [index(['slug']), index(['authorId'])], routes: { defaults: { auth: 'userAuth' }, list: { auth: 'none' }, get: { auth: 'none' }, },});
const PostOps = defineOperations(Post, { byAuthor: op.lookup({ fields: { authorId: 'param:authorId' }, returns: 'many' }), bySlug: op.lookup({ fields: { slug: 'param:slug' }, returns: 'one' }), publish: op.transition({ field: 'status', from: 'draft', to: 'published', match: { id: 'param:id' }, }),});
// --- Custom routes ---
const previewBody = z.object({ markdown: z.string() });
// --- Package ---
export const blogPackage = definePackage({ name: 'blog', mountPath: '/blog', dependencies: ['slingshot-auth'], entities: [entity({ config: Post, operations: PostOps })], domains: [ domain({ name: 'extras', basePath: '/extras', routes: [ route.post({ path: '/preview', auth: 'userAuth', summary: 'Preview rendered markdown', request: { body: previewBody }, handler: ({ body, respond }) => respond.json({ html: `<article>${body.markdown}</article>` }), }), route.get({ path: '/health', auth: 'none', summary: 'Blog package health check', handler: ({ respond }) => respond.json({ ok: true }), }), ], }), ], publicPaths: ['/blog/extras/health'],});This package produces:
- Five CRUD routes for posts at
/blog/posts/* - Three operation routes (
by-author,by-slug,publish) - Two custom routes (
/blog/extras/preview,/blog/extras/health) - All auth-protected except reads and health
Package options reference
Section titled “Package options reference”| Field | Required | What it does |
|---|---|---|
name | yes | Stable identity for ordering, diagnostics, and dependencies |
mountPath | no | Base path for all package routes (default: /) |
dependencies | no | Other packages or plugins that must boot first |
entities | no | Entity modules owned by this package |
domains | no | Non-entity route groups owned by this package |
middleware | no | Named middleware available to package routes |
capabilities | no | Published and required cross-package contracts |
tenantExemptPaths | no | Paths that bypass tenant resolution |
csrfExemptPaths | no | Paths that bypass CSRF enforcement |
publicPaths | no | Paths exposed without auth |
Mount paths
Section titled “Mount paths”The mountPath prefix applies to all routes in the package:
const adminPackage = definePackage({ name: 'admin', mountPath: '/admin', domains: [ domain({ name: 'dashboard', basePath: '/dashboard', routes: [ route.get({ path: '/stats', // final path: /admin/dashboard/stats auth: 'userAuth', handler: ({ respond }) => respond.json({ users: 42 }), }), ], }), ],});Entity routes also respect the mount path: a Post entity in a package with mountPath: '/blog'
generates routes at /blog/posts/*.
Dependencies
Section titled “Dependencies”Packages can declare dependencies on other packages or plugins:
// @skip-typecheckconst commentPackage = definePackage({ name: 'comments', dependencies: ['blog', 'slingshot-auth'], entities: [entity({ config: Comment })],});Dependencies ensure:
- Packages boot in the correct order
- A clear error if a required dependency is missing
- Cross-package contracts are explicit
Package middleware
Section titled “Package middleware”Middleware defined in a package is available to that package’s routes by name:
// @skip-typecheckimport { getActor } from '@lastshotlabs/slingshot';
const adminPackage = definePackage({ name: 'admin', middleware: { requireAdmin: async (c, next) => { const actor = getActor(c); if (!actor.roles?.includes('admin')) { return c.json({ error: 'Forbidden' }, 403); } await next(); }, auditLog: async (c, next) => { console.log(`[audit] ${c.req.method} ${c.req.path}`); await next(); }, }, domains: [ domain({ name: 'admin', routes: [ route.post({ path: '/users/:id/ban', auth: 'userAuth', middleware: ['requireAdmin', 'auditLog'], handler: async ({ params, respond }) => { // ban user logic return respond.json({ banned: params.id }); }, }), ], }), ],});Composing packages in the app root
Section titled “Composing packages in the app root”The app root reads like a table of contents:
// @skip-typecheckimport { defineApp } from '@lastshotlabs/slingshot';import { createAuthPlugin } from '@lastshotlabs/slingshot-auth';import { adminPackage } from './src/packages/admin';import { blogPackage } from './src/packages/blog';import { commentPackage } from './src/packages/comments';
export default defineApp({ port: 3000, security: { signing: { secret: process.env.JWT_SECRET! } }, db: { default: { adapter: 'postgres', url: process.env.DATABASE_URL! } }, plugins: [createAuthPlugin({ auth: { roles: ['user', 'admin'], defaultRole: 'user' } })], packages: [blogPackage, commentPackage, adminPackage],});Each package is self-contained. Adding a new feature means adding a new package to the array.
Inspect a package
Section titled “Inspect a package”Use inspectPackage to see the effective routes and structure without reading framework internals:
// @skip-typecheckimport { inspectPackage } from '@lastshotlabs/slingshot';import { blogPackage } from './packages/blog';
const inspection = inspectPackage(blogPackage);
console.log(inspection.name); // 'blog'console.log(inspection.mountPath); // '/blog'console.log(inspection.dependencies); // ['slingshot-auth']
// See all entitiesfor (const e of inspection.entities) { console.log(e.entityName, e.resolvedPath, e.wiringMode);}
// See all routesfor (const d of inspection.domains) { for (const r of d.routeDetails) { console.log(r.method.toUpperCase(), r.resolvedPath, r.auth ?? 'default'); }}
// See capabilitiesconsole.log(inspection.capabilities.provides);console.log(inspection.capabilities.requires);This is useful for:
- Tests that verify package structure
- Docs tooling that generates route maps
- Debugging boot issues in large apps
Next in your journey
Section titled “Next in your journey”You have a package. The next two steps shape its surface area:
- Domains and Routes — typed custom routes inside the package, for endpoints that don’t map to entity CRUD.
- Package Contracts — when another package needs to consume yours, declare a typed public surface so the boundary is explicit and validated at boot.
After that:
- Events and the Event Bus — decouple packages by publishing typed events.
- Plugin Interface — when you need framework-level infrastructure with lifecycle hooks.