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

GET

/Book/

List all books (HTML table or JSON)

GET

/Book/create

Show create form

POST

/Book/create

Create a book

GET

/Book/:id

View a book

POST

/Book/:id/update

Update a book

POST

/Book/:id/delete

Delete a book

Prerequisites

  • The paradox CLI on your PATH

  • paradox-node installed (npm install paradox-node)

  • A running PostgreSQL database

  • Node.js with TypeScript (tsc)

paradox-node requires a PostgreSQL connection. By default it connects to postgres://localhost:5432/paradox. Use --database-url to override.

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:

  1. Generate spec — paradox generate --spec produces the typed AST as JSON

  2. Generate TypeScript — paradox generate --typescript produces types and validation

  3. Generate DDL — paradox generate --postgresql produces CREATE TABLE statements

  4. Generate server — Produces store.ts (pg Pool) and main.ts (Express app with CRUD routes and Pug views)

  5. Compile — Runs tsc on the generated TypeScript

  6. 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.

Health Check

curl http://localhost:3000/

Returns the app manifest with title, author, and available types.

Next Steps