One file is the whole resource.
A model declaration emits the table, the row type, the validator, and the route scaffolds. Change a field once — every layer that touches it updates.
File-based routing. Typed models. Scaffolded CRUD.
bun add -g @hopak/cli Why Hopak?
A model declaration emits the table, the row type, the validator, and the route scaffolds. Change a field once — every layer that touches it updates.
No decorator scanner, no DI container, no routes synthesised at boot. What's in app/ is exactly what the server executes — open any file and rewrite it.
SQLite ships with Bun. Switch to Postgres or MySQL with hopak use <dialect> — model code never moves.
Two commands scaffold the project plus six REST endpoints. Validation, JSON, pagination, sensitive-field hiding — included, nothing to glue.
JWT + OAuth + RBAC in @hopak/auth. Typed migrations with up / down. Dev certs via hopak generate cert. No four-package combo to assemble.
bun:sqlite, Bun.password, Bun.serve, Bun.write — the framework reaches for the runtime that is already there.
How it works
Fields, constraints, relations.
import { model, text, boolean, belongsTo } from '@hopak/core';
export default model('post', {
title: text().required().min(3).max(200),
content: text().required(),
published: boolean().default(false),
author: belongsTo('user'),
}); The declaration creates the table columns, the TypeScript row type, and the validator.
Two files. Six endpoints.
import { crud } from '@hopak/core';
import post from '../../models/post';
export const GET = crud.list(post);
export const POST = crud.create(post); The generated files are regular TypeScript. Edit an endpoint or remove it at any time.
A request against the running server.
{
"items": [
{ "id": 1, "title": "Hello", "published": true, "author_id": 4 }
],
"total": 1,
"limit": 20,
"offset": 0
} The server validates input, hides sensitive fields, and paginates list endpoints.