Start Zero-1.
Describe what data your frontend needs.
$ ANTHROPIC_API_KEY=sk-ant-... bun run index.ts
✓ Zero-1 running → http://localhost:7777
✓ Web console → http://localhost:7777/__generate
✓ MCP server → bun run mcp.ts
3 resources · 270 records
· products 20 records
· customers 50 records
· orders 200 records
↳ orders.customerId → customers.id
↳ orders.customerName ← customers.name (denormalized, auto-synced)
Or wire Zero-1 into your AI agent via MCP — the agent gets every operation as a callable tool.
{ "mcpServers": { "z1": { "command": "bun", "args": ["run", "/path/to/z1/mcp.ts"] } } }
⚠ orders.customerName carries a copy of this field
200 records would go stale — syncing first, then renaming
✓ customers.name → fullName
✓ orders.customerName 200 records re-synced
✓ orders.shippingAddress added — 200 records seeded
It's a real API.
Curl it.
Every resource you describe becomes a live endpoint with full CRUD — GET, POST, PUT, PATCH, DELETE. IDs are auto-assigned. FK references are always valid. Data is generated from field names, not random garbage.
$ curl 'localhost:7777/posts?sort=likes:desc&limit=3'
[
{ "id": 42, "title": "How I built my side project", "likes": 2841, "published": true },
{ "id": 7, "title": "10 lessons from shipping fast", "likes": 1204, "published": true },
{ "id": 23, "title": "Why I chose Bun over Node", "likes": 891, "published": true }
]
# Filter by field + free-text search, stacked
$ curl 'localhost:7777/posts?role=admin&q=launch&sort=likes:desc&limit=10'
# Create — id is auto-assigned
$ curl -X POST localhost:7777/posts \
-H 'Content-Type: application/json' \
-d '{"userId": 3, "title": "My first post", "likes": 0}'
{ "id": 201, "userId": 3, "title": "My first post", "likes": 0 }
# Partial update
$ curl -X PATCH localhost:7777/posts/201 \
-H 'Content-Type: application/json' \
-d '{"likes": 42}'
{ "id": 201, "userId": 3, "title": "My first post", "likes": 42 }
# Delete returns 204 No Content
$ curl -X DELETE localhost:7777/posts/201
Filtering, sorting, text search, and pagination are all described in natural language when you set up a scenario — the query behaviour follows from how you described the data. The query API reflects exactly what you asked for.
Works with whatever
you're already using.
Zero-1 is plain HTTP — point your data layer at localhost:7777 and it works. No SDK, no adapter, no wrapper. Swap the base URL when your real backend is ready.
// Swap BASE when your real backend is ready const BASE = 'http://localhost:7777'; // Fetch a list const posts = await fetch(`${BASE}/posts?sort=likes:desc&limit=10`) .then(r => r.json()); // Create a record const created = await fetch(`${BASE}/posts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 1, title: 'Hello', likes: 0 }), }).then(r => r.json()); // Delete await fetch(`${BASE}/posts/42`, { method: 'DELETE' });
import { useState, useEffect } from 'react'; const BASE = 'http://localhost:7777'; export default function PostList() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetch(`${BASE}/posts?sort=likes:desc`) .then(r => r.json()) .then(data => { setPosts(data); setLoading(false); }); }, []); if (loading) return <p>Loading…</p>; return posts.map(p => ( <div key={p.id}>{p.title}</div> )); }
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; const BASE = 'http://localhost:7777'; export default function PostList() { const qc = useQueryClient(); const { data: posts = [], isLoading } = useQuery({ queryKey: ['posts'], queryFn: () => fetch(`${BASE}/posts?sort=likes:desc`).then(r => r.json()), }); const create = useMutation({ mutationFn: (body) => fetch(`${BASE}/posts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }).then(r => r.json()), onSuccess: () => qc.invalidateQueries({ queryKey: ['posts'] }), }); }
<script setup> import { ref, onMounted } from 'vue'; const BASE = 'http://localhost:7777'; const posts = ref([]); onMounted(async () => { posts.value = await fetch(`${BASE}/posts?sort=likes:desc`) .then(r => r.json()); }); async function create(payload) { const post = await fetch(`${BASE}/posts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }).then(r => r.json()); posts.value.unshift(post); } </script>
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; const BASE = 'http://localhost:7777'; @Injectable({ providedIn: 'root' }) export class PostsService { constructor(private http: HttpClient) {} list(): Observable<Post[]> { return this.http.get<Post[]>(`${BASE}/posts?sort=likes:desc`); } create(body: Partial<Post>): Observable<Post> { return this.http.post<Post>(`${BASE}/posts`, body); } }
Data that reacts.
Not just sits there.
Endpoints can react to each other. Describe the side-effect logic inline in your scenario — create an order and stock decrements, post a comment and the count increments, delete a user and their content is removed. Zero-1 wires it so your mock API behaves like the real thing.
$ curl localhost:7777/products/12
{ "id": 12, "name": "Wireless Headphones", "stock": 7, "status": "in_stock" }
$ curl -X POST localhost:7777/orders \
-H 'Content-Type: application/json' \
-d '{"productId": 12, "quantity": 7}'
{ "id": 88, "productId": 12, "quantity": 7, "status": "confirmed" }
$ curl localhost:7777/products/12
{ "id": 12, "stock": 0, "status": "out_of_stock" }
stock went from 7 → 0, status flipped automatically
Change the schema
while it's running.
Describe what changed. Zero-1 sends only the schema to Claude (not the data), applies the diff, and backfills all existing records locally — O(records) time, flat ~1024-token API call regardless of dataset size.
# Add, rename, and remove fields
$ curl -X POST localhost:7777/__generate/evolve \
-H 'Content-Type: application/json' \
-d '{"name": "posts", "changes": "add a views counter, rename likes to reactions, drop published"}'
{
"updated": 200,
"added": ["views"],
"renamed": { "likes": "reactions" },
"removed": ["published"]
}
# Add an auth guard inline — no separate /configure call needed
$ curl -X POST localhost:7777/__generate/evolve \
-H 'Content-Type: application/json' \
-d '{"name": "posts", "changes": "require X-API-Key on all write requests"}'
{ "updated": 200, "headerRules": 1 }
# Add chaos inline too
$ curl -X POST localhost:7777/__generate/evolve \
-H 'Content-Type: application/json' \
-d '{"name": "orders", "changes": "add a status field (pending/confirmed/cancelled). fail 10% of POSTs with 503."}'
{ "updated": 80, "added": ["status"], "chaosRules": 1 }
Added fields get context-aware generated values — views gets realistic view counts, email gets real-looking addresses. Unknown fields fall back to type-appropriate random values.
Want pagination?
Describe what you need — cursor, offset, or page-based — in plain English. Every response field name is under your control. Zero-1 configures the envelope so it matches whatever your real API will produce.
$ curl 'localhost:7777/posts?limit=20&sort=createdAt:desc'
{
"items": [
{ "id": 200, "title": "Shipping without a backend", "reactions": 1204 },
{ "id": 199, "title": "Why I prototype with mock APIs", "reactions": 892 },
...
],
"nextToken": "eyJpZCI6MTgwfQ",
"total": 200
}
Need a header guard
on a route?
Describe the rule — bearer token, API key, custom header. Zero-1 enforces it. Your frontend gets real 401s to handle. Guards can also inject response headers on every matching request, so you can simulate X-Request-Id, WWW-Authenticate, rate limit headers — whatever your real API sends back.
$ curl -X POST localhost:7777/posts \
-d '{"userId": 3, "title": "test"}'
{ "error": "Missing or invalid Authorization header" } ← 401
$ curl -X POST localhost:7777/posts \
-H 'Authorization: Bearer my-token' \
-d '{"userId": 3, "title": "test"}'
{ "id": 202, "userId": 3, "title": "test", "likes": 0 } ← 201
JWT claims.
Isolate records by user.
Map a JWT claim to a resource field and Zero-1 enforces per-user record isolation automatically. No middleware to write — pass a Bearer token, get back only the records that belong to you.
# Declare: "for /posts, filter by userId matching the JWT sub claim"
$ curl -X POST localhost:7777/__generate/scoping \
-H 'Content-Type: application/json' \
-d '{"resource": "posts", "field": "userId", "claim": "sub"}'
{ "id": 1, "resource": "posts", "field": "userId", "claim": "sub" }
# GET with a JWT whose "sub" is "42" → only posts where userId=42
$ curl localhost:7777/posts \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...' # sub=42
[
{ "id": 7, "userId": 42, "title": "My post" },
{ "id": 15, "userId": 42, "title": "Another post" }
]
# GET /posts/3 where post.userId ≠ 42 → 403
$ curl localhost:7777/posts/3 \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...'
{ "error": "Forbidden." } ← 403
# POST auto-populates the owner field from the JWT claim
$ curl -X POST localhost:7777/posts \
-H 'Authorization: Bearer ...' \
-d '{"title": "New post"}'
{ "id": 201, "userId": 42, "title": "New post" } ← userId injected from JWT
# No JWT? Scoping rules are skipped — open access
$ curl localhost:7777/posts ← returns all records
| Method | Path | Description |
|---|---|---|
| GET | /__generate/scoping | List all active scoping rules |
| POST | /__generate/scoping | Add a rule: { resource, field, claim } |
| DELETE | /__generate/scoping | Clear all scoping rules |
| DELETE | /__generate/scoping/:id | Remove a specific rule |
Inline the parent.
Expand any FK field.
Add ?expand=field to any GET request and Zero-1 replaces the FK value with the full parent record. Multiple fields, comma-separated. No N+1 — all resolved in one pass.
# Without expansion
$ curl localhost:7777/orders/1
{ "id": 1, "userId": 5, "productId": 12, "total": 49.99 }
# With expansion — FK IDs replaced with full parent records
$ curl 'localhost:7777/orders/1?expand=userId,productId'
{
"id": 1,
"userId": { "id": 5, "name": "Alice Chen", "email": "alice@example.com" },
"productId": { "id": 12, "name": "Wireless Headphones", "price": 49.99 },
"total": 49.99
}
# Works on collection endpoints too
$ curl 'localhost:7777/orders?expand=userId'
[ { "id": 1, "userId": { "id": 5, "name": "Alice Chen" }, ... } ]
Expansion uses the live knowledge graph to resolve FK edges. If the FK target resource doesn't exist or the referenced ID is not found, the field is left as-is.
Upload a file.
Get a real URL back.
POST multipart form data to any resource endpoint. Zero-1 stores the bytes in memory, replaces the file field with a /__uploads/{id} URL, and serves the original bytes with the correct Content-Type when fetched.
# Upload an avatar image alongside the user record
$ curl -X POST localhost:7777/users \
-F 'name=Alice Chen' \
-F 'role=admin' \
-F 'avatar=@photo.jpg'
{
"id": 42,
"name": "Alice Chen",
"role": "admin",
"avatar": "http://localhost:7777/__uploads/a3f8c1d2"
}
# The URL serves the real image bytes
$ curl localhost:7777/__uploads/a3f8c1d2 > avatar.jpg
# Works on PATCH too — replace an existing file field
$ curl -X PATCH localhost:7777/users/42 \
-F 'avatar=@new-photo.png'
{ "id": 42, "avatar": "http://localhost:7777/__uploads/b9d4e7f1" }
File bytes live in memory alongside all other Zero-1 state. They are included in fork snapshots and global export/import. Uploads are served at GET /__uploads/:id with the original Content-Type and filename.
Full OAuth 2.0 provider,
from a description.
Describe the flow. Zero-1 runs a real OAuth 2.0 / OIDC provider locally — auth code, PKCE, client credentials, scopes, refresh tokens, introspection, OIDC discovery. No JWT library, no crypto package. Token signing via Web Crypto API (HS256).
# Configure from a NL description
$ curl -X POST localhost:7777/__generate/oauth/configure \
-H 'Content-Type: application/json' \
-d '{"description": "authorization code flow with PKCE. scopes: read, write, admin. three clients: web-app (read/write), mobile (read), admin-tool (admin). JWT tokens, 1h expiry."}'
{ "flow": "authorization_code", "pkceRequired": true, "scopes": ["read","write","admin"], "clients": 3 }
# Start the auth code flow — auto-approves, returns code in redirect
$ curl -G 'localhost:7777/__generate/oauth/authorize' \
--data-urlencode 'response_type=code' \
--data-urlencode 'client_id=web-app' \
--data-urlencode 'redirect_uri=http://localhost:3000/callback' \
--data-urlencode 'code_challenge=abc123' \
--data-urlencode 'code_challenge_method=S256' \
--data-urlencode 'scope=read write'
→ 302 Location: http://localhost:3000/callback?code=xyz_auth_code
# Exchange the code for a token
$ curl -X POST localhost:7777/__generate/oauth/token \
-d 'grant_type=authorization_code&code=xyz_auth_code&code_verifier=my_verifier&client_id=web-app'
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write",
"refresh_token": "rt_..."
}
# Introspect to verify
$ curl -X POST localhost:7777/__generate/oauth/introspect \
-d 'token=eyJhbGciOiJIUzI1NiJ9...'
{ "active": true, "scope": "read write", "client_id": "web-app", "exp": 1735689600 }
# OIDC discovery — auto-generated from config
$ curl localhost:7777/__generate/oauth/.well-known/openid-configuration
Registered: /authorize · /token · /revoke · /introspect · /userinfo · /.well-known/openid-configuration · /.well-known/jwks.json. Per-client scope enforcement included.
Make it fail
on purpose.
Inject failures, latency, and rate limits into any endpoint. Describe the chaos inline in your scenario, or add it on top of a live API. Rules are additive — latency stacks, error rates evaluate independently. Chaos never affects /__generate/* meta endpoints.
$ curl -X POST localhost:7777/comments \
-d '{"postId": 42, "body": "great post"}'
{ "error": "Service Unavailable" } ← 503 · 20% of POSTs fail
$ curl -X POST localhost:7777/comments \
-d '{"postId": 42, "body": "great post"}'
{ "id": 401, "postId": 42, "body": "great post" } ← 201 · retry succeeded
$ curl localhost:7777/posts ← responds in 100–400ms
[...]
Build your API
directly in Cursor.
Zero-1 ships a zero-config MCP server. Add it to Cursor, VS Code, or any MCP-compatible editor and create endpoints, inject chaos, fork workspaces — all from the chat panel, without leaving your editor.
ANTHROPIC_API_KEY=sk-ant-... bun run index.ts — server starts on port 7777.~/.cursor/mcp.json (or .claude/mcp.json for Claude Code):{ "mcpServers": { "z1": { "command": "bun", "args": ["run", "/path/to/z1/mcp.ts"] } } }
You
i'm building a reviews table. i need an endpoint
with productId, userId, rating 1–5, body text
and a verified flag. seed 50 rows.
Claude
⠸ calling create_scenario...
Done. Your endpoint is live at localhost:7777/reviews
Sample record:
{ "id": 1, "productId": 12, "userId": 4,
"rating": 4, "body": "Solid build, fast shipping.",
"verified": true }
Point your table component at that URL.
Supports ?sort=rating:desc ?q= ?limit= ?page=
You
add 15% slow responses so i can test my
skeleton loader actually shows up.
Claude
⠸ calling add_chaos...
Done. GET /reviews delays 200–800ms on 15% of hits.
Refresh a few times and your loader will trigger.
❯ █
Fork the state.
Wreck it safely.
Clone the entire running state into an isolated workspace. Route requests with X-Workspace and only that fork mutates.
$ curl -X POST localhost:7777/__generate/fork \
-d '{"name": "qa-branch"}'
{ "id": "a7f3b2c1", "name": "qa-branch", "records": 650 }
$ curl -X DELETE localhost:7777/posts/42 \
-H 'X-Workspace: a7f3b2c1' ← only fork mutates
$ curl localhost:7777/posts/42
{ "id": 42, "title": "How I built my side project" } ← still there
See how everything
connects.
Zero-1 maintains a live knowledge graph of every resource, foreign-key edge, and denormalized field copy it has observed. Zero config — it builds itself from traffic. Open the visual explorer or query the JSON API.
# Full graph dump — resources, FK edges, provenance edges
$ curl localhost:7777/__generate/graph
{
"resources": [ { "name": "users", "fields": [ ... ] }, ... ],
"fkEdges": [
{ "from": "orders", "fromField": "userId", "to": "users", "toField": "id" }
],
"provenanceEdges": [
{ "child": "orders", "childField": "buyerName", "parent": "users", "parentField": "name", "viaFK": "userId" }
]
}
# Impact analysis before a schema change
$ curl -X POST localhost:7777/__generate/graph/analyze \
-d '{"resource": "users", "fields": ["id"], "typeChange": {"field": "id", "fromType": "number", "toType": "string"}}'
{
"safe": false,
"fkImpact": [ { "resource": "orders", "field": "userId", "reason": "FK references users.id" } ],
"summary": "1 FK reference(s) would break: orders.userId."
}
Open /__generate/graph-view for an interactive D3 force graph — click any node or edge for details, drag to rearrange, live auto-refresh every 3 seconds.
| Method | Path | Description |
|---|---|---|
| GET | /__generate/graph | Full graph: nodes, FK edges, provenance edges |
| POST | /__generate/graph/analyze | Impact report: { resource, fields[], typeChange? } |
| GET | /__generate/graph-view | Interactive D3 visualization |
Import a spec.
Export your state.
Point Zero-1 at an existing OpenAPI or Swagger spec and it imports the resources directly. Export your current state as JSON to share with a teammate or restore later.
$ curl -X POST localhost:7777/__generate/openapi \
-d '{"url": "https://petstore3.swagger.io/api/v3/openapi.json"}'
{ "imported": ["pets", "store", "users"], "records": 90 }
$ curl localhost:7777/__generate/export > session.json
$ curl -X POST localhost:7777/__generate/import -d @session.json
{ "imported": 3, "message": "State restored." }
What are you building?
Type what your project actually needs. These are examples.