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.
What a package gives you
Section titled “What a package gives you”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.
Before and after
Section titled “Before and after”Without packages — everything is tangled:
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:
// @skip-typecheckimport { 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.
Your first package
Section titled “Your first package”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.
What should be a package
Section titled “What should be a package”| You are building… | Package structure |
|---|---|
| A blog feature | Post entity + custom routes for preview/feed |
| User profiles | Profile entity + avatar upload route |
| An admin dashboard | Admin routes + middleware for role checks |
| Payment processing | Custom routes + webhook handler |
| Notifications | Notification entity + SSE delivery routes |
| A search feature | Custom search routes + event subscribers |
Packages with dependencies
Section titled “Packages with dependencies”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.
When NOT to use a package
Section titled “When NOT to use a package”- 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 adefinePackage. 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
Read in order
Section titled “Read in order”Primary path
Section titled “Primary path”Lower-level surfaces
Section titled “Lower-level surfaces”Reach for these only when the primary path is overkill, when working with legacy code, or when you need a true escape hatch.
Framework-level plugins
Section titled “Framework-level plugins”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.
Where entities fit
Section titled “Where entities fit”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.
Next in your journey
Section titled “Next in your journey”The pages in this section build on each other in this order:
- definePackage — declare a package and the modules it owns.
- Domains and Routes — typed custom routes inside the package.
- 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.