Skip to content

First Steps

This page covers the concrete shape of a Slingshot app: file layout, the minimal config, and what runs at boot. If you want the conceptual map first, read Introduction. If you want to skip straight to a runnable app, jump to Quick Start.

The framework requires exactly one file: app.config.ts. Everything else is convention. Most apps end up looking something like this:

my-app/
├── app.config.ts ← required — declares your app
├── package.json
└── src/
└── packages/
├── blog/
│ ├── index.ts ← exports the blogPackage definition
│ ├── post.entity.ts ← Post entity
│ ├── post.operations.ts ← publish, archive, etc.
│ ├── routes.ts ← custom domain routes (preview, feed)
│ └── public.ts ← Package Contract (capabilities, public refs)
└── notifications/
├── index.ts
└── public.ts

This is one common shape, not required. Each package owns its own entities, routes, and public contract — they don’t live in top-level entities/ or routes/ folders. If your project is tiny or you prefer a flat layout, the framework won’t stop you. The only hard rule is: app.config.ts exists at the project root and exports defineApp({...}) as default.

app.config.ts is the source of truth for what your app is: which packages it loads, which database it talks to, what middleware runs, which port it binds.

app.config.ts
import { defineApp, definePackage, domain, route } from '@lastshotlabs/slingshot';
export default defineApp({
port: 3000,
packages: [
definePackage({
name: 'app',
domains: [
domain({
name: 'health',
basePath: '/health',
routes: [
route.get({
path: '/',
summary: 'Health check',
handler: ({ respond }) => respond.json({ ok: true }),
}),
],
}),
],
}),
],
});

This is a complete app. It has one route, a typed handler, validation, and an OpenAPI spec.

Terminal window
slingshot start

The framework discovers app.config.ts, imports its default export, and starts an HTTP server. Three URLs are immediately available:

URLWhat
http://localhost:3000/healthYour route
http://localhost:3000/openapi.jsonOpenAPI spec
http://localhost:3000/docsInteractive API docs

defineApp is a typed identity function — a single line of code:

export const defineApp = <T extends object>(config: AppConfig<T>): AppConfig<T> => config;

It exists so your config object gets full autocomplete and type inference without you having to annotate it. The CLI loads the file, takes the default export, and hands it to the runtime.

That means:

  • You can read your config back with import config from './app.config' — it’s just an object
  • You can compose it programmatically — split it across files, derive parts conditionally
  • You can test it without booting anything — just import it

When slingshot start boots your config, the framework:

  1. Validates the config object
  2. Resolves secrets from the configured provider
  3. Connects databases and infrastructure
  4. Runs every plugin’s setupMiddleware in dependency order
  5. Mounts package routes, generated entity routes, and routesDir files
  6. Registers OpenAPI specs and /docs
  7. Starts the HTTP server, plus WebSocket and SSE endpoints if configured
  8. Wires graceful shutdown for SIGTERM / SIGINT

You get all of this without writing any of it.

You have an app running. Now follow the canonical journey:

  1. Composing an App — the mental model for packages, custom routes, contracts, events, and plugins. This is the journey hub; every page below extends it.
  2. Packages — turn your app into self-contained feature modules.
  3. Authoring Routes — per-route concerns once you have routes worth tightening.

Reference for when you need it:

  • App Config — every section of defineApp, what it does, when to set it.
  • Entities — declare a data model, get CRUD for free.