pREST now speaks MySQL. Version 2.5 serves any MySQL 8.0.19+ database as a REST API. Set engine = "mysql" and point it at your database.
Every table gets CRUD, filters, paging, joins and JWT auth, and AI agents can read it over MCP. Install v2.5.1, the current release.
pREST v2 series: Architecture (rc6) → v2.0.0 / v2.1.0 GA → MCP tutorial → AI plugins → v2.2.0 → v2.3.0 → v2.4.0 → v2.4.1 + v2.4.2 → This post (v2.5: MySQL)
New to pREST? The pREST v2 guide lists every release and where to start.
Table of Contents
- What you get
- Quickstart
- Query and write
- Auth and permissions
- AI agents over MCP
- MySQL and Postgres together
- What’s different on MySQL
- Versions and limits
- Also in v2.5
- FAQ
- Also from Insurgency Labs
What you get
Point pREST at an existing MySQL database. You write no code and run no migrations.
- GET, POST, PATCH and DELETE on every table.
- Filters in the query string:
?qty=$gt.0,?id=$in.1,3. - Paging, sorting, column selection, counts and joins.
- JWT auth and a per-column allowlist in one TOML file.
- An MCP endpoint, so AI agents can read your data.
- Custom SQL endpoints from
.sqlfiles.
It fits internal tools, admin panels and quick APIs over a database you already have. It doesn’t support MariaDB or TiDB.
Quickstart
You need Docker and curl. Start MySQL first:
docker network create prest-demo
docker run -d --name mysql --network prest-demo \
-e MYSQL_ROOT_PASSWORD=rootpw -e MYSQL_DATABASE=shop \
-e MYSQL_USER=prest -e MYSQL_PASSWORD=prest \
mysql:8.4
# wait until MySQL accepts connections
until docker exec mysql mysqladmin ping -uroot -prootpw --silent; do sleep 2; done
Add a table with a few rows:
docker exec -i mysql mysql -uprest -pprest shop <<'SQL'
CREATE TABLE items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
qty INT NOT NULL DEFAULT 0,
price DECIMAL(10,2),
meta JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO items (name, qty, price, meta) VALUES
('hat', 2, 19.90, JSON_OBJECT('kind', 'apparel')),
('mug', 10, 8.50, JSON_OBJECT('kind', 'kitchen')),
('scarf', 0, 24.00, JSON_OBJECT('kind', 'apparel'));
SQL
Start pREST and call it:
docker run -d --name prest --network prest-demo -p 3000:3000 \
-e PREST_ENGINE=mysql \
-e PREST_DB_URL='mysql://prest:prest@mysql:3306/shop' \
-e PREST_JWT_DEFAULT=false \
prest/prest:v2.5.1
curl -s localhost:3000/_health
# {"adapter":"mysql"}
curl -s localhost:3000/shop/shop/items
# [{"created_at":"2026-10-09T02:36:16","id":1,"meta":{"kind":"apparel"},"name":"hat","price":"19.90","qty":2},
# {"created_at":"2026-10-09T02:36:16","id":2,"meta":{"kind":"kitchen"},"name":"mug","price":"8.50","qty":10},
# {"created_at":"2026-10-09T02:36:16","id":3,"meta":{"kind":"apparel"},"name":"scarf","price":"24.00","qty":0}]
The first start takes about 15 seconds. The path is /{connection}/{database}/{table}, and here both names are shop. PREST_JWT_DEFAULT=false turns auth off for the demo; we turn it on below. The image runs on Intel and Apple Silicon.
Query and write
Reads use query-string filters:
# items in stock
curl -s 'localhost:3000/shop/shop/items?qty=$gt.0&_select=id,name,qty'
# [{"id":1,"name":"hat","qty":2},{"id":2,"name":"mug","qty":10}]
# pick rows by id
curl -s 'localhost:3000/shop/shop/items?id=$in.1,3&_select=id,name'
# [{"id":1,"name":"hat"},{"id":3,"name":"scarf"}]
# case-insensitive match
curl -s 'localhost:3000/shop/shop/items?name=$ilike.HAT&_select=id,name'
# [{"id":1,"name":"hat"}]
# sort and page
curl -s 'localhost:3000/shop/shop/items?_order=-price&_page=1&_page_size=2&_select=name,price'
# [{"name":"scarf","price":"24.00"},{"name":"hat","price":"19.90"}]
# count
curl -s 'localhost:3000/shop/shop/items?_count=*'
# [{"count":3}]
# filter on a JSON field
curl -s 'localhost:3000/shop/shop/items?meta->>kind:jsonb=apparel&_select=id,name'
# [{"id":1,"name":"hat"},{"id":3,"name":"scarf"}]
Writes take JSON bodies:
# insert one row
curl -s -X POST localhost:3000/shop/shop/items -H 'Content-Type: application/json' \
-d '{"name":"socks","qty":5,"price":6.00,"meta":{"kind":"apparel"}}'
# {"created_at":"2026-10-09T02:36:46","id":4,"meta":{"kind":"apparel"},"name":"socks","price":"6.00","qty":5}
# update
curl -s -X PATCH 'localhost:3000/shop/shop/items?name=socks' \
-H 'Content-Type: application/json' -d '{"qty":7}'
# {"rows_affected":1}
# update and return the row
curl -s -X PATCH 'localhost:3000/shop/shop/items?name=socks&_returning=*' \
-H 'Content-Type: application/json' -d '{"qty":8}'
# [{"created_at":"2026-10-09T02:36:46","id":4,"meta":{"kind":"apparel"},"name":"socks","price":"6.00","qty":8}]
# delete
curl -s -X DELETE 'localhost:3000/shop/shop/items?name=socks'
# {"rows_affected":1}
# insert many rows at once
curl -s -X POST localhost:3000/batch/shop/shop/items -H 'Content-Type: application/json' \
-d '[{"name":"cap","qty":1},{"name":"bowl","qty":3,"price":12.00}]'
# [{"created_at":"2026-10-09T02:36:46","id":5,"meta":null,"name":"cap","price":null,"qty":1},
# {"created_at":"2026-10-09T02:36:46","id":6,"meta":null,"name":"bowl","price":"12.00","qty":3}]
$ilike matched HAT because MySQL collations ignore case by default. POST returns the full row with its new id. PATCH returns a count unless you add _returning=*. Batch records can have different fields, and missing ones get their defaults.
Auth and permissions
Turn on login and limit what each table exposes. One file does both. Save this as prest.toml:
engine = "mysql"
[pg]
url = "mysql://prest:prest@mysql:3306/shop"
[auth]
enabled = true
[jwt]
key = "replace-with-a-random-secret-of-at-least-32-bytes"
algo = "HS256"
[access]
restrict = true
[[access.tables]]
name = "items"
permissions = ["read"]
fields = ["id", "name", "qty"]
Restart pREST with it, add a user, and log in:
docker rm -f prest
docker run -d --name prest --network prest-demo -p 3000:3000 \
-v "$PWD/prest.toml:/app/prest.toml:ro" prest/prest:v2.5.1
# pREST creates the prest_users table; add a user with a bcrypt hash
HASH=$(docker run --rm httpd:2.4-alpine htpasswd -bnBC 10 "" s3cret | tr -d ':\n')
docker exec -i mysql mysql -uprest -pprest shop \
-e "INSERT INTO prest_users (name, username, password) VALUES ('Ada', 'ada', '$HASH')"
TOKEN=$(curl -s -X POST localhost:3000/auth -H 'Content-Type: application/json' \
-d '{"username":"ada","password":"s3cret"}' | jq -r .token)
curl -s localhost:3000/shop/shop/items -H "Authorization: Bearer $TOKEN"
# [{"id":1,"name":"hat","qty":2},{"id":2,"name":"mug","qty":10},{"id":3,"name":"scarf","qty":0},
# {"id":5,"name":"cap","qty":1},{"id":6,"name":"bowl","qty":3}]
curl -s 'localhost:3000/shop/shop/items?_select=price' -H "Authorization: Bearer $TOKEN"
# {"error":"you don't have permission for this action, please check the permitted fields for this table"}
curl -s -X POST localhost:3000/shop/shop/items -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"name":"x"}'
# {"error": "authorization required"}
Reads return only id, name and qty. Asking for price fails, and so does any write. In production, give pREST its own MySQL user and grant it one database, not root.
AI agents over MCP
The same server answers MCP at /_mcp, and agents get read-only tools. With the token from above:
curl -s -X POST localhost:3000/_mcp -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq -r '.result.tools[].name'
# prest.describe_table
# prest.list_databases
# prest.list_schemas
# prest.list_tables
# prest.select.shop.shop.items
# prest.select_table
curl -s -X POST localhost:3000/_mcp -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"prest.select_table","arguments":{"database":"shop","schema":"shop","table":"items","filters":{"name":"hat"},"limit":5}}}'
# {"jsonrpc":"2.0","id":2,"result":{"database":"shop","schema":"shop","table":"items",
# "columns":["id","name","qty"],"rows":[{"id":1,"name":"hat","qty":2}],"count":1}}
The agent sees the same columns as REST, so price stays hidden. Asking for it returns unsupported column: price. Without a token, /_mcp answers 401. To connect Cursor or Claude, follow the MCP tutorial.
MySQL and Postgres together
One pREST can serve both engines. Give each database an alias:
[[databases]]
alias = "app"
url = "postgres://postgres:postgres@pg:5432/app?sslmode=disable"
[[databases]]
alias = "shop"
engine = "mysql"
url = "mysql://prest:prest@mysql:3306/shop"
The first path segment picks the database:
curl -s localhost:3000/app/public/users
# [{"id": 1, "email": "ada@example.com"}, {"id": 2, "email": "linus@example.com"}]
curl -s 'localhost:3000/shop/shop/items?_select=id,name&_page=1&_page_size=2'
# [{"id":1,"name":"hat"},{"id":2,"name":"mug"}]
Auth and permissions work the same on both. Each MySQL alias only serves the database in its URL. It helps during a migration, or when one API sits over an old app and a new one.
What’s different on MySQL
Most requests work the same. These don’t:
| Postgres | MySQL | |
|---|---|---|
| PATCH / DELETE | Returns rows | Returns a count; add _returning=* for rows |
$ilike |
Always ignores case | Follows the column collation |
$all, ltree, $tsquery, vectors |
Supported | unsupported operator |
FULL JOIN |
Supported | Not available |
DECIMAL |
Number | String ("19.90") |
DATETIME |
With time zone | No time zone |
{schema} in the URL |
A schema | The MySQL database |
_QUERIES SQL files |
$1, "name" |
?, `name` |
Full list: DIFFERENCES.md.
Versions and limits
- MySQL 8.0.19 or newer. CI tests 8.0, 8.4 and 26.7.
- MariaDB, TiDB and Aurora MySQL aren’t supported.
- TLS modes are
disable,requireandskip-verify. Client certificates aren’t supported yet. - Set
[mysql] prepare = trueif you want server-side prepared statements.
Also in v2.5
_joinnow works withaccess.restrict = true, and joined tables return their allowed columns. Thanks to Roshan931 for the first fix (#364).- Middleware plugins load again (#948), and the Docker image builds them on start.
- Upgrading from v2.4? Postgres configs work as before. Pull
prest/prest:v2.5.1.
FAQ
How do I turn a MySQL database into a REST API without code?
Run pREST v2.5.1 with engine = "mysql" and your database URL. Every table gets REST endpoints with filters and paging. The quickstart above takes about five minutes.
Which MySQL versions does pREST support?
MySQL 8.0.19 and newer. CI tests 8.0, 8.4 and 26.7.
Does pREST work with MariaDB or Aurora MySQL?
No. This adapter targets MySQL 8 only. MariaDB, TiDB and Aurora MySQL aren’t supported.
Can one pREST serve MySQL and PostgreSQL?
Yes. Add each database under [[databases]] with an alias. Set engine = "mysql" on the MySQL ones.
Can AI agents query MySQL through pREST?
Yes, through the MCP endpoint at /_mcp. Agents get read-only tools. They see only the columns your permissions allow.
Is pREST free?
Yes. It’s MIT licensed, and the Docker images are free.
Release notes: v2.5.1. Code: github.com/prest/prest.
Using Postgres instead? See pREST in 10 minutes. Something broke on your MySQL? Open an issue with your version and the request.
Also from Insurgency Labs
Building internal tools on MySQL? DeliveryCompass shows PR cycle time and review patterns from GitHub, with read-only access. See the DeliveryCompass overview. It’s free during Early Access. It doesn’t replace Jira or measure deploy-based DORA.