Tutorial: Building a Bookshelf App
Build a full-stack bookshelf application with CRUD routes, HTML views, and a PostgreSQL backend from a Paradox specification. You will define types in .dox, and paradox-node generates everything: TypeScript types, database schema, Express server, and Pug templates.
What You Will Build
A complete web application with:
| Method | Path | Description |
|---|---|---|
|
|
List all books (HTML table or JSON) |
|
|
Show create form |
|
|
Create a book |
|
|
View a book |
|
|
Update a book |
|
|
Delete a book |
Prerequisites
-
The
paradoxCLI on yourPATH -
paradox-nodeinstalled (npm install paradox-node) -
A running PostgreSQL database
-
Node.js with TypeScript (
tsc)
|
|
Step 1: Write the Specification
Create bookshelf.dox:
import std
type Book
title: Text
author: Text
year: Integer
type CreateBookRequest
title: Text
author: Text
year: Integer
valid CreateBookRequest
title != "" | Title is required
author != "" | Author is required
year > 0 | Year must be positive
For paradox-node, you only need types — not URI constants. The --crud flag generates all CRUD routes automatically.
Step 2: Examine the Generated TypeScript
paradox generate --typescript
This produces .dox/index.ts with your types and validation:
export interface Book {
title: string;
author: string;
year: number;
}
export interface CreateBookRequest {
title: string;
author: string;
year: number;
}
export const validCreateBookRequest = (input: unknown): CreateBookRequest => {
// ... structural checks ...
const x = input as CreateBookRequest;
const errors: string[] = [];
if (x.title === "") errors.push(`Title is required`);
if (x.author === "") errors.push(`Author is required`);
if (x.year <= 0) errors.push(`Year must be positive`);
if (errors.length > 0) throw new Error(errors.join('; '));
return x;
}
Step 3: Examine the Generated DDL
paradox generate --postgresql
This produces .dox/module.sql:
-- Generated by Paradox SQL (PostgreSQL dialect)
-- Do not edit by hand
CREATE TABLE "Book" (
"id" BIGSERIAL PRIMARY KEY,
"title" TEXT NOT NULL,
"author" TEXT NOT NULL,
"year" BIGINT NOT NULL
);
CREATE TABLE "CreateBookRequest" (
"id" BIGSERIAL PRIMARY KEY,
"title" TEXT NOT NULL,
"author" TEXT NOT NULL,
"year" BIGINT NOT NULL,
CONSTRAINT "CreateBookRequest_check_1" CHECK ("title" != ''),
CONSTRAINT "CreateBookRequest_check_2" CHECK ("author" != ''),
CONSTRAINT "CreateBookRequest_check_3" CHECK ("year" > 0)
);
Paradox generates a table for every product type in the spec, not just --crud targets. The CreateBookRequest table appears because it is a product type — only Book will have CRUD routes wired up. Validation rules become PostgreSQL CHECK constraints, enforcing them at the database level as well.
Step 4: Run the Generator
paradox-node --port 3000 --title "Bookshelf" --crud Book
This runs a six-stage pipeline:
-
Generate spec —
paradox generate --specproduces the typed AST as JSON -
Generate TypeScript —
paradox generate --typescriptproduces types and validation -
Generate DDL —
paradox generate --postgresqlproducesCREATE TABLEstatements -
Generate server — Produces
store.ts(pg Pool) andmain.ts(Express app with CRUD routes and Pug views) -
Compile — Runs
tscon the generated TypeScript -
Run — Starts the server, initializing the database schema on boot
Step 5: Test It
HTML Interface
Open http://localhost:3000/Book/ in your browser to see the list view. Click "Create" to add books through the HTML form. The form fields are generated from the Book type’s fields.
JSON API
# Create a book
curl -X POST http://localhost:3000/Book/create \
-H "Content-Type: application/json" \
-d '{"title": "Dune", "author": "Frank Herbert", "year": 1965}'
# List all books (JSON)
curl http://localhost:3000/Book/ -H "Accept: application/json"
# Get a book
curl http://localhost:3000/Book/1 -H "Accept: application/json"
# Validation error -- empty title
curl -X POST http://localhost:3000/Book/create \
-H "Content-Type: application/json" \
-d '{"title": "", "author": "Nobody", "year": 2024}'
# => 400 Bad Request: Title is required
All routes support both JSON and HTML responses. When the Accept header is application/json, the response is JSON. Otherwise, Pug templates render full HTML pages.
Next Steps
-
Reference — Full flag reference, authentication, permissions, and extra routes
-
TypeScript Code Generation — Details on generated types and validation
-
PostgreSQL Code Generation — Details on generated DDL
-
Express Tutorial — Lighter-weight Express scaffold (handlers only, no database)
-
Servant Tutorial — The same API in Haskell
-
Gloo-net Tutorial — Generate a Rust/WASM client