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

--title

(required)

The name of your app (used in the health check manifest)

--author

null

Author name (included in manifest)

--uris

(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:

  1. Generate TypeScript types — Runs paradox generate --typescript to produce type definitions in .dox/

  2. Evaluate URIs to JSON — For each --uris name, runs paradox generate --json <name> to get the structured URI data

  3. Parse URI configs — Extracts path, meta (HTTP method), io (input/output types), and accept (MIME types) from each URI

  4. Generate server code — Produces handlers.ts (the interface you implement) and server.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

/users/${Integer}

/users/:integer

/posts/${Text id}

/posts/:id

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

application/json

express.json() parser, res.json() response

text/plain

express.text() parser, res.type('text/plain').send() response

text/html

res.type('text/html').send() response

application/octet-stream

express.raw() parser

application/x-www-form-urlencoded

express.urlencoded() parser

createServer Options

createServer(handlers: Handlers, opts?: {
  app?: Express;           // Bring your own Express app
  middleware?: RequestHandler[];  // Additional middleware to apply
}): Express
  • Pass app to mount routes onto an existing Express app instead of creating a new one

  • Pass middleware to inject custom middleware (auth, logging, CORS) before the generated routes

See Also