paradox-express
Generate type-safe Express.js servers from Paradox URI specifications.
Define your endpoints in .dox, and paradox-express generates a fully wired Express server with typed handlers, request validation, and route setup.
Usage
paradox-express --title "My API" --uris endpoints
All flags:
| Flag | Default | Description |
|---|---|---|
|
(required) |
The name of your app (used in the health check manifest) |
|
|
Author name (included in manifest) |
|
(required, repeatable) |
Name of a spec constant that evaluates to a URI or a product of URIs |
The --uris flag can be repeated to include endpoints from multiple constants:
paradox-express --title "My API" --uris publicApi --uris adminApi
Example
Given this .dox specification:
import std
type User
id: Text
name: Text
email: Text
type CreateUserRequest
name: Text
email: Text
type CreateUserResponse
id: Text
status: Text
createUser: URI
http://localhost/users POST (CreateUserRequest. CreateUserResponse)
getUser: URI
http://localhost/users GET (Unit. User)
Run:
paradox-express --title "User Service" --uris createUser --uris getUser
This generates two files:
.dox/handlers.ts
import type { Request, Response } from 'express';
export interface Handlers {
createUser: (req: Request, res: Response) => void | Promise<void>;
getUser: (req: Request, res: Response) => void | Promise<void>;
}
.dox/server.ts
import express, { Express, RequestHandler } from "express";
import type { Handlers } from "./handlers";
export const manifest = {
title: "User Service",
author: null,
endpoints: [
{ method: "POST", path: "/users", accept: ["application/json"] },
{ method: "GET", path: "/users", accept: ["application/json"] }
],
};
export const createServer = (
handlers: Handlers,
opts?: { app?: Express; middleware?: RequestHandler[] }
): Express => {
const app = opts?.app ?? express();
// ... middleware setup, routes, error handling
return app;
};
Wiring It Up
import { createServer } from "./.dox/server";
import type { Handlers } from "./.dox/handlers";
const handlers: Handlers = {
createUser: async (req, res) => {
// Your implementation here
res.json({ id: "1", status: "created" });
},
getUser: async (req, res) => {
res.json({ id: "1", name: "Isaac", email: "isaac@example.com" });
},
};
const app = createServer(handlers);
app.listen(3000, () => console.log("Server running on :3000"));
How It Works
paradox-express operates in four stages:
-
Generate TypeScript types — Runs
paradox generate --typescriptto produce type definitions in.dox/ -
Evaluate URIs to JSON — For each
--urisname, runsparadox generate --json <name>to get the structured URI data -
Parse URI configs — Extracts
path,meta(HTTP method),io(input/output types), andaccept(MIME types) from each URI -
Generate server code — Produces
handlers.ts(the interface you implement) andserver.ts(the wired Express app)
URI Constants vs Products of URIs
The --uris flag accepts either a single URI constant or a product type whose fields are URIs:
| Single URI -- pass as --uris createUser
createUser: URI
http://localhost/users POST (CreateUserRequest. CreateUserResponse)
| Product of URIs -- pass as --uris api
type ApiEndpoints
createUser: URI
getUser: URI
deleteUser: URI
api: ApiEndpoints
ApiEndpoints:
createUser: http://localhost/users POST (CreateUserRequest. CreateUserResponse)
getUser: http://localhost/users GET (Unit. User)
deleteUser: http://localhost/users/${Integer} DELETE (Unit. Unit)
Path Parameters
Interpolated type captures in URI paths are converted to Express :param style:
.dox Path |
Express Route |
|---|---|
|
|
|
|
Request Validation
For POST, PUT, and PATCH endpoints with an input type, the generated route calls the corresponding valid<Type> function (from paradox generate --typescript) on req.body before passing it to your handler.
Health Check
Every generated server includes a GET /health endpoint that returns the manifest:
{
"status": "ok",
"title": "User Service",
"author": null,
"endpoints": [...]
}
Error Handling
Generated routes catch errors from handlers. Errors with name === 'ValidationError' return 400, errors with a status property return that status, and everything else returns 500.
Content Types
The accept field controls both response formatting and body parser middleware:
| MIME Type | Behavior |
|---|---|
|
|
|
|
|
|
|
|
|
|
createServer Options
createServer(handlers: Handlers, opts?: {
app?: Express; // Bring your own Express app
middleware?: RequestHandler[]; // Additional middleware to apply
}): Express
-
Pass
appto mount routes onto an existing Express app instead of creating a new one -
Pass
middlewareto inject custom middleware (auth, logging, CORS) before the generated routes
See Also
-
URI Types — URI literal syntax and structure
-
TypeScript Code Generation — The types that Express handlers use
-
paradox-servant — Haskell Servant equivalent
-
paradox-node — Full-stack CRUD server with PostgreSQL
-
paradox-gloo-net — Rust/WASM HTTP client