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.