Skip to content

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.

src/packages/blog.ts
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
FieldRequiredWhat it does
nameyesStable identity for ordering, diagnostics, and dependencies
mountPathnoBase path for all package routes (default: /)
dependenciesnoOther packages or plugins that must boot first
entitiesnoEntity modules owned by this package
domainsnoNon-entity route groups owned by this package
middlewarenoNamed middleware available to package routes
capabilitiesnoPublished and required cross-package contracts
tenantExemptPathsnoPaths that bypass tenant resolution
csrfExemptPathsnoPaths that bypass CSRF enforcement
publicPathsnoPaths exposed without auth

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/*.

Packages can declare dependencies on other packages or plugins:

// @skip-typecheck
const 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

Middleware defined in a package is available to that package’s routes by name:

// @skip-typecheck
import { 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 });
},
}),
],
}),
],
});

The app root reads like a table of contents:

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

Use inspectPackage to see the effective routes and structure without reading framework internals:

// @skip-typecheck
import { 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 entities
for (const e of inspection.entities) {
console.log(e.entityName, e.resolvedPath, e.wiringMode);
}
// See all routes
for (const d of inspection.domains) {
for (const r of d.routeDetails) {
console.log(r.method.toUpperCase(), r.resolvedPath, r.auth ?? 'default');
}
}
// See capabilities
console.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

You have a package. The next two steps shape its surface area:

  1. Domains and Routes — typed custom routes inside the package, for endpoints that don’t map to entity CRUD.
  2. 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: