Skip to content

Package-First Authoring

Your app starts as one file with a few routes. Then it grows. You add auth, then blog posts, then comments, then notifications, then admin endpoints.

At some point, having everything in one file stops working. That is when you reach for packages.

A package is a feature boundary. It owns:

  • its routes
  • its entities
  • its middleware
  • its mount path

The app root just composes packages. It does not implement features.

Without packages — everything is tangled:

app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
export default defineApp({
port: 3000,
// 200 lines of routes, middleware, entities mixed together
});

With packages — each feature is self-contained:

app.config.ts
// @skip-typecheck
import { defineApp } from '@lastshotlabs/slingshot';
import { adminPackage } from './src/packages/admin';
import { authPackage } from './src/packages/auth';
import { blogPackage } from './src/packages/blog';
export default defineApp({
port: 3000,
packages: [blogPackage, authPackage, adminPackage],
});

Each package file is focused. The app root reads like a table of contents.

src/packages/blog.ts
import { defineEntity, definePackage, domain, entity, field, route } from '@lastshotlabs/slingshot';
const Post = defineEntity('Post', {
namespace: 'blog',
fields: {
id: field.string({ primary: true, default: 'uuid' }),
title: field.string(),
body: field.string(),
published: field.boolean({ default: false }),
},
});
export const blogPackage = definePackage({
name: 'blog',
entities: [entity({ config: Post })],
domains: [
domain({
name: 'blog-extras',
basePath: '/blog',
routes: [
route.get({
path: '/featured',
auth: 'none',
summary: 'Get featured posts',
handler: async ({ respond }) => {
// Custom logic that does not fit the entity model
return respond.json({ posts: [] });
},
}),
],
}),
],
});

The blog package owns entity routes (generated CRUD for posts) AND custom routes (/blog/featured). Both live inside the same package boundary.

You are building…Package structure
A blog featurePost entity + custom routes for preview/feed
User profilesProfile entity + avatar upload route
An admin dashboardAdmin routes + middleware for role checks
Payment processingCustom routes + webhook handler
NotificationsNotification entity + SSE delivery routes
A search featureCustom search routes + event subscribers

Packages can declare dependencies on other packages:

export const commentPackage = definePackage({
name: 'comments',
dependencies: ['blog', 'slingshot-auth'],
entities: [entity({ config: Comment })],
});

Dependencies ensure packages boot in the right order and make cross-package contracts explicit.

  • One-off routes — if you just need a health check, put it in the app root
  • Framework-level plugins — auth, persistence, queues, custom middleware, anything that needs lifecycle hooks or registrar registrations is a SlingshotPlugin, not a definePackage. See Plugin Interface. Both are first-class authoring tiers; pick by the shape of what you’re building, not by perceived level.
  • A single entity with no custom routes — a package with just one entity is fine, but do not create package boundaries for complexity’s sake

Reach for these only when the primary path is overkill, when working with legacy code, or when you need a true escape hatch.

For auth, persistence, queues, search, or any infrastructure with async bootstrap and registrar registrations, write a SlingshotPlugin instead of a package. It’s a parallel first-class authoring tier — see Plugin Interface and Composing an App for the full distinction.

If your package owns data (posts, users, orders, comments), define an entity inside it. The Entity System generates CRUD routes and operations from a single definition.

The pages in this section build on each other in this order:

  1. definePackage — declare a package and the modules it owns.
  2. Domains and Routes — typed custom routes inside the package.
  3. Package Contracts — declare the package’s typed public surface so other packages can consume it.

After that, return to Composing an App for the full reading order — events, plugins, and the per-route layer come next.