generaptr

Point it at your database. Get a working API.

Generaptr reads the tables of a MySQL or PostgreSQL database and writes an Express 5 + Drizzle API, with validation, an OpenAPI 3.1 document that describes it, and a web app to browse and edit the rows.

npx generaptr-cli

Node 22 or later. The CLI asks for the connection, then writes the API and the web app.

Your table
CREATE TABLE users (
  id         serial PRIMARY KEY,
  email      varchar(50) NOT NULL UNIQUE,
  active     boolean NOT NULL DEFAULT true,
  created_at timestamptz NOT NULL
             DEFAULT now()
);
Your API: src/db/schema.js and its routes
export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  email: varchar("email", { length: 50 }).notNull(),
  active: boolean("active").notNull()
    .default(sql.raw("true")),
  createdAt: timestamp("created_at", { withTimezone: true })
    .notNull().default(sql.raw("now()")),
}, (table) => [
  unique("users_email_key").on(table.email),
]);
GET/users?limit=20&offset=0
POST/users 201, Location: /users/1
GET/users/{id}?with=accounts 404 when missing
PUT/users/{id} 204
DELETE/users/{id} 204
Your web app, from the same tables

The generated API

api/
├── package.json        # exact versions of express, drizzle-orm, the driver, zod
├── drizzle.config.js
├── .env.example        # DATABASE_URL
├── openapi.json
├── scripts/baseline.js # npm run db:baseline
└── src/
    ├── index.js
    ├── app.js          # one router per table
    ├── db/schema.js    # one Drizzle table per database table
    ├── db/relations.js # both sides of every foreign key
    └── routes/users.js # list, get, create, update, delete
  • Each table with one primary key gets five routes. Link tables with a composite key get none.
  • The list and get routes take with to include related rows: GET /users/1?with=accounts,groups. A foreign key accounts.user_id gives accounts.user and users.accounts, and a link table such as users_groups gives users.groups. A nested list returns at most 100 rows, and with reaches only tables that have routes.
  • zod schemas from drizzle-orm/zod check each request body. A bad body gets 400 with the list of problems.
  • Database errors get a generic 500. The response never shows the database message.
  • Defaults come from the database, so a client can leave out the columns that the database fills. Identity and generated columns are not in the request bodies.
  • src/db/schema.js rebuilds the database it came from: keys with their names and actions, CHECK constraints, every index, generated and identity columns, collations, enums, and sequences. A test rebuilds a schema from it and compares the catalogs of both databases.
cd api
cp .env.example .env    # set DATABASE_URL
npm start

The generated web app

web/
├── package.json        # exact versions of react, vite, tailwindcss, @json-render/*
├── vite.config.js      # forwards /api to API_URL
├── .env.example        # API_URL
└── src/
    ├── pages.json      # one json-render spec per page, from openapi.json
    ├── main.jsx        # renders the specs; the same for every API
    ├── theme.css       # colors and radius for light and dark mode
    └── index.css
The list of users in light mode A user in dark mode, with a JSON value as a tree
  • Each table with routes gets a list with paging, a detail page with its related rows, and forms to create and edit a row. A filter in the sidebar finds a table.
  • The pages are json-render specs drawn with shadcn components and Tailwind. Edit src/pages.json to change a page.
  • A foreign key links to its row on a detail page, and is a select of rows in a form. JSON values show as a tree that folds.
  • Forms check required fields, lengths, and JSON as you type, and show the API's message when the API rejects a row.
  • A switch in the sidebar sets light mode, dark mode, or the system mode. To change the colors, edit src/theme.css, or paste a theme from ui.shadcn.com/themes.
  • The browser calls /api on the web app's own server, which forwards to the API, so the API needs no CORS.
cd web
cp .env.example .env    # set API_URL
npm run dev

Read from a database or a spec

A database

MySQL through information_schema, PostgreSQL through pg_catalog. The user needs read access only.

An OpenAPI 3.1 file, in YAML or JSON

A file that generaptr wrote reads back to the same tables. Other files map by convention: each object schema becomes a table.

From either source, generaptr writes the API and the web app, or only openapi.json. An OpenAPI file can target MySQL or PostgreSQL, so you can move a schema from one engine to the other.

Migrations

The generated API uses Drizzle Kit. Migrations go forward only: to undo a change, write a new migration.

npm run db:generate   # writes SQL from src/db/schema.js
npm run db:migrate    # applies the SQL
Before the first migration: on the database that generaptr read, run npm run db:baseline once. It writes the first migration and records it as applied without running it, because those tables exist there already. A new, empty database skips this step: db:migrate creates the tables.

Column types

MySQLPostgreSQL
integer types, signed or unsignedsmallint, integer, bigint, serial and identity
tinyint(1) as booleanboolean
decimal(p,s)numeric(p,s)
varchar(n), char(n)character varying(n), character(n)
enum(...)enum types
text, date and time types with fractional seconds, json, float, doubletext, date, time, timestamp with precision and time zone, json, jsonb, uuid, real, double precision
one-dimensional arrays, such as text[]
binary, varbinary, blob typesbytea

Binary columns carry base64 strings in JSON. Any other type, such as tsvector, inet, money, bit(3), MySQL set(...), or a multi-dimensional array, becomes a Drizzle custom type with the exact SQL type.

Generaptr stops with Unsafe name on a table or column name that has characters other than letters, digits, and _.

Use it from code

The generaptr package holds the same steps as the CLI.

import { handlers, operations } from 'generaptr';

const db = new handlers.Postgres({
  host: '127.0.0.1', port: 5432, user: 'postgres',
  password: 'secret', database: 'shop', schema: 'public',
});
const schema = await db.readSchema();
await db.close();

await operations.api.generateApi('./shop-api', info, schema);
await operations.web.generateWeb('./shop-web', info, schema);
await operations.spec.generateSpec('./spec', info, schema);

info holds the name, version, description, author, and license of the generated project. The package README lists every export.

Known limits

Drizzle cannot write these, so generaptr stops with an error when a schema has one:

  • PostgreSQL: INCLUDE columns on an index, unique indexes with NULLS NOT DISTINCT, exclusion constraints, virtual generated columns, and a sequence of a type other than bigint that no column owns.
  • MySQL: FULLTEXT and SPATIAL indexes, and enum or set values with a backslash.

Generaptr reads one PostgreSQL schema, public by default. It does not read views, functions, triggers, or comments.

Coming from 2.x? Keys now belong to the table, identity columns read as identity, and MySQL binary columns carry base64. The README lists the steps.