Skip to main content
zerotal

Getting Started

Zerotal is a full-stack framework for Bun. This guide walks you from a blank machine to a running app with a route, a model, and a service provider.

Requirements

  • Bun ≥ 1.3.14 — install
  • A database, unless you scaffold the Minimal template. Every other template is configured for SQLite, which needs nothing installed — PostgreSQL and MySQL are supported and are a DATABASE_URL away.

Create a new project

# in your project root's parent directory
bun create zerotal my-app    # or: bunx create-zerotal my-app
cd my-app

The scaffolder prompts for a project name and a template, then generates a ready-to-run project and installs dependencies. The database prompt only appears for the API template:

PromptOptions
Project nameDefaults to my-zerotal-app
TemplateAPI, Admin, Flow, React, Vue, or Minimal (see below)
DatabaseSQLite (zero setup), PostgreSQL, or MySQL — asked for API

It writes a fresh APP_KEY into the generated .env.example for you (no manual generation needed). For Postgres/MySQL it reminds you to set DATABASE_URL before migrating.

Without a terminal — CI, scripts, agents

Every prompt has a flag, and the scaffolder never asks a question when there is no TTY to answer it:

bunx create-zerotal my-app --template=api --db=postgres --no-install
bunx create-zerotal my-app --yes            # take the defaults for anything unset
bunx create-zerotal --help
FlagValues / description
-t, --template <name>api, admin, flow, react, vue, minimal
--db <name>sqlite, postgres, mysql — API template only
-y, --yesTake defaults for anything not given; never prompt
--no-installSkip bun install
-h, --helpUsage
-v, --versionThe scaffolder's own version

An answer that is missing and cannot be asked for is an error naming the flag that would supply it, and the exit code is non-zero — so a pipeline fails where it used to wait. A failed bun install also exits non-zero when there is no terminal: a half-built project that reports success is worse than one that stops.

Which template should I use?

  • API — JSON REST API with core, ORM, auth, validation, and testing. The default for a backend service.
  • Admin — an admin panel: resources, sign-in, dashboard widgets, and seeded demo data. bun zt db:seed gives you an account to log in with.
  • Flow — server-driven reactive UI (Flow pages, top nav, Tailwind).
  • ReactInertia + React SPA with file-based routes and Tailwind.
  • Vue — Inertia + Vue SPA with file-based routes and Tailwind.
  • Minimal — a single page with JSX views and Tailwind on the bare framework.

Note — every template except Minimal ships a database config and migrations, configured for SQLite. Only the API template asks which database you want; the rest start on SQLite and switch when you set DATABASE_URL.

Project structure

The API template generates the following. Other templates vary (SPA templates add a resources/ frontend, for example):

# my-app/ (API template)
my-app/
├── app/
│   ├── controllers/        # HTTP controllers
│   ├── middleware/          # Custom middleware
│   └── models/              # ORM models (auto-discovered)
├── bootstrap/
│   └── app.ts               # Builds the Application (providers + routing)
├── config/                  # Config files (database, session, queue…) — auto-loaded
├── database/
│   └── migrations/          # Database migrations
├── routes/
│   └── index.ts             # Route definitions
├── tests/                   # Test suites
├── zt.ts                   # Managed CLI entry point — do not edit
└── .env                     # Environment variables (git-ignored)

zt.ts is the entry point for everythingbun zt serve, migrate, test, and every make:* generator run through it. It boots bootstrap/app.ts, which defines your Application. Anything under app/models/, app/providers/, and config/ is auto-discovered at boot — no manual registration.

Tipapp/providers/ is auto-discovered when present. The API template doesn't generate one; run bun zt make:provider … and the framework picks it up at boot.

Environment setup

The scaffolder generates .env.example with a ready-made APP_KEY. Copy it to .env:

# in your project root
cp .env.example .env
# .env
APP_ENV=development
APP_KEY=base64:…              # pre-generated; rotate with `bun zt key:generate`
DATABASE_URL=./database/db.sqlite
SESSION_SECRET=change-me-in-production

The database is configured with a single DATABASE_URL — SQLite uses a file path, Postgres/MySQL use a connection string:

# .env — alternative DATABASE_URL forms
# DATABASE_URL=postgres://user:pass@localhost:5432/my_app
# DATABASE_URL=mysql://root@localhost:3306/my_app

Danger — Change SESSION_SECRET before deploying. The placeholder value is not safe for production.

See Configuration for the full config system.

Run migrations

# in your project root
bun zt migrate

Start the dev server

# in your project root
bun dev          # → bun zt serve --dev

Your app is now running at http://localhost:3000 with hot reload enabled — --dev watches your files and hot-swaps routes without dropping connections. The generated package.json also gives you bun start (production serve) and bun test.

Your first route

Open routes/index.ts and add:

// routes/index.ts
import { Router } from "zerotal";
import { PostController } from "../app/controllers/PostController.ts";

Router.get("/posts", PostController, "index");
Router.post("/posts", PostController, "store");

Create the controller:

// app/controllers/PostController.ts
import type { HttpContext } from "zerotal";

export class PostController {
  async index(ctx: HttpContext) {
    return ctx.json({ posts: [] });
  }

  async store(ctx: HttpContext) {
    const body = await ctx.request.json();
    return ctx.json({ created: body }, 201);
  }
}

Note — Every controller action receives the request HttpContext directly. Read route params and resolved model bindings from ctx.params; type them with the generic, e.g. ctx: HttpContext<{ post: Post }>.

Your first model

# in your project root
bun zt make:model Post --migration

This generates app/models/Post.ts and a matching migration. Models in app/models/ are auto-discovered at boot — no manual registration, and the table name is derived by convention (Postposts), so @table is optional. The same applies to observers, policies, and event listeners; see Conventions.

Open the migration and define your columns:

// database/migrations/xxxx_create_posts_table.ts
import { Migration, Schema } from "@zerotal/orm";

export default class CreatePostsTable extends Migration {
  async up(): Promise<void> {
    await Schema.create("posts", (table) => {
      table.increments("id");
      table.string("title");
      table.text("body");
      table.string("slug").unique();
      table.timestamps();
    });
  }

  async down(): Promise<void> {
    await Schema.drop("posts");
  }
}

Run it:

# in your project root
bun zt migrate

Your model is ready to use:

// in a controller or service
import { Post } from "../models/Post.ts";

const posts = await Post.query().latest().limit(10).get();
const post = await Post.find(1);
const fresh = await Post.create({ title: "Hello", body: "...", slug: "hello" });

Adding a service provider

Providers are where you wire up your own bindings. Scaffold one:

# in your project root
bun zt make:provider AppServiceProvider
// app/providers/AppServiceProvider.ts
import { ServiceProvider } from "zerotal";
import { PaymentGateway } from "../services/PaymentGateway.ts";
import { StripeGateway } from "../services/StripeGateway.ts";

export class AppServiceProvider extends ServiceProvider {
  onRegister(): void {
    this.app.container.singleton(
      PaymentGateway,
      () => new StripeGateway({ key: Bun.env.STRIPE_KEY! }),
    );
  }
}

Anything under app/providers/ is auto-discovered and registered at boot — no manual wiring needed. You only touch bootstrap/app.ts when you need to control ordering (e.g. one provider must register before another). See Service Providers and Conventions.

Available commands

Everything runs through zt.ts. The most common:

# in your project root
bun zt make:model Name --migration   # model + migration
bun zt make:controller Name          # controller class
bun zt make:middleware Name          # middleware class
bun zt make:provider Name            # service provider
bun zt make:job Name                 # queue job
bun zt migrate                       # run pending migrations
bun zt migrate:rollback              # roll back last batch
bun zt migrate:status                # show migration status
bun zt migrate:fresh                 # roll back all + re-migrate
bun zt key:generate                  # generate a fresh APP_KEY
bun zt queue:work                    # start the queue worker

There are many more (make:policy, make:factory, make:seeder, make:page, db:seed, schedule:list, …). Run bun zt list to see them all, or see Commands for the full list and Scaffolding for what each generator produces.

Next steps

  • Routing — route definitions, groups, parameters, and file-based routes.
  • Controllers — move route logic into classes.
  • ORM — models, queries, and relationships.
  • Configuration — how config and auto-discovery work.
  • Flow — server-driven reactive UI (or Inertia for a React/Vue SPA).