Docs / Quick start

Quick start

From empty folder to typed REST endpoint in under five minutes.

Install the CLI, scaffold a project, generate a model and its CRUD routes, then hit the endpoints with curl. Everything below assumes the default SQLite — the runtime behavior is identical on every dialect.

Scaffold and run

bun add -g @hopak/cli
hopak new my-app           # SQLite by default (zero-install)
cd my-app
hopak dev

Want Postgres or MySQL from the start? Pick the dialect at creation time — the driver gets installed, hopak.config.ts is pre-set, and .env.example already has DATABASE_URL:

hopak new my-app --db postgres
hopak new my-app --db mysql
hopak new my-app --db sqlite   # explicit opt-in (default)

Already inside a project? Switch dialects:

hopak use postgres         # installs `postgres` driver, patches config, updates .env.example
hopak use mysql            # installs `mysql2`, etc.
hopak use sqlite           # back to default

Server on http://localhost:3000. Scaffold a model + its REST files with two commands (hopak generate model/crud) and you get validation, JSON serialization, static files — zero runtime magic, every route is in source.

Create a REST resource

Goal: expose GET/POST /api/posts and GET/PUT/PATCH/DELETE /api/posts/:id.

1. Generate the model + CRUD route files:

hopak generate model post
hopak generate crud post

The first command writes app/models/post.ts. The second writes two route files — app/routes/api/posts.ts (list + create) and app/routes/api/posts/[id].ts (read + replace + patch + delete). Open either file; the entire REST surface is there as plain code you can read and edit — nothing is synthesized at runtime.

2. Add your fields to the model:

// app/models/post.ts
import { model, text, boolean } from '@hopak/core';

export default model('post', {
  title: text().required().min(3),
  content: text().required(),
  published: boolean().default(false),
});

3. Start the server:

hopak dev

On first boot Hopak creates the SQLite file at .hopak/data.db and runs CREATE TABLE IF NOT EXISTS for every model. Safe to repeat — hopak sync does the same thing explicitly if you prefer to separate schema sync from server start (handy for CI or a fresh Postgres / MySQL database).

4. Try it from another terminal:

curl -X POST http://localhost:3000/api/posts \
  -H 'content-type: application/json' \
  -d '{"title":"Hello","content":"World"}'

Expected response (201 Created):

{ "id": 1, "title": "Hello", "content": "World", "published": false,
  "createdAt": "...", "updatedAt": "..." }

5. List them:

curl http://localhost:3000/api/posts
# → { "items": [...], "total": 1, "limit": 20, "offset": 0 }

curl 'http://localhost:3000/api/posts?limit=5&offset=10'
# pagination via query string; limit defaults to 20, max 100

6. Verify what’s actually registered:

hopak check
# ✓ Models   1 loaded (post)
# ✓ Routes   6 file route(s)

Six endpoints from two generated files: list, read, create, replace, patch, delete — all paginated and validated. The plural segment (/api/posts) comes from pluralize('post') — irregular plurals are handled (story → stories, box → boxes). Don’t want endpoints for a given model? Just don’t run hopak generate crud for it — the model still becomes a table, you just don’t expose HTTP routes.

Next: Project layout.