04 — API PLATFORM
Depends on: 03_DATABASE.md · Next: 05_UI_UX.md
1. Principle: API-First
UI → API SDK (typed) → REST /v1 → Validation → Service → Prisma → MariaDB
Frontend never imports Prisma. Everything documented in OpenAPI 3.1.
- Base:
https://yas.sh/api/v1(single-host, co-located — no api.yas.sh subdomain) - Redirect:
https://yas.sh/:code(single canonical — no go.yas.sh) - Docs:
https://yas.sh/docs(Stripe-like)
2. Stack
- Runtime: Next.js Route Handlers (Phase 1) or Hono 4.x (when split)
- Validation: Zod v4 schemas shared with frontend +
zod-openapifor docs - Spec: OpenAPI 3.1 generated from code, served at
/v1/docs/openapi.json, UI at/v1/docs - Auth:
Better Authsessions (cookie__Host-session) + API keys (Authorization: Bearer yas_live_...) - Errors: RFC 9457
application/problem+jsonwith stable codes
3. Conventions
- Versioning: URL
/v1/; breaking →/v2/; additive is minor - Pagination: Cursor
?limit=20&cursor=abc→{ data, nextCursor } - Filtering:
?search=...&tag=...&sort=createdAt:desc - Idempotency:
Idempotency-Key: <uuid>on POST/links,/bulk - Request ID:
x-request-idpropagated to logs - Rate limit headers:
RateLimit-Limit,RateLimit-Remaining,Retry-Afteron 429
4. Auth & Rate Limits
| Group | Auth | Limit (Phase 1) |
|---|---|---|
Public redirect GET /:code |
none | 1000 req/min IP (Nginx + memory) |
Auth POST /v1/auth/* |
none | 10 req/min IP |
Links POST /v1/links |
session or API key | 60 req/min user/key |
Analytics GET /v1/analytics/* |
session or API key | 120 req/min |
QR GET /v1/qr |
none / optional | 60 req/min IP |
Scopes on API keys: links:read, links:write, analytics:read, qr:write.
5. Error Format (RFC 9457)
{
"type": "https://yas.sh/docs/errors#rate_limited",
"title": "Rate Limited",
"status": 429,
"code": "RATE_LIMITED",
"detail": "Too many requests. Retry after 42s.",
"instance": "/v1/links",
"retryAfter": 42
}
Codes: UNAUTHORIZED, FORBIDDEN, NOT_FOUND, VALIDATION_ERROR, RATE_LIMITED, CONFLICT, EXPIRED, PASSWORD_REQUIRED.
6. Phase 1 — 5 Full API Groups (Implemented)
Group A: Links
POST /v1/links → create (idempotent)
GET /v1/links → list (paginated, filter, search)
GET /v1/links/:id → get one
PATCH /v1/links/:id → edit (owner only, versioned)
DELETE /v1/links/:id → delete
POST /v1/links/bulk → bulk create (up to 100)
GET /v1/links/export.csv → CSV export (injection-safe)
GET /v1/links/duplicate?url= → duplicate check
Group B: Redirect
GET /:code → 302/301 redirect, bot-filtered count, increment
Query: ?password= → if protected
Group C: Analytics
GET /v1/analytics/overview?days=30 → totals, chart data
GET /v1/analytics/:linkId → per-link breakdown (country/device/referrer)
Group D: QR
GET /v1/qr?url=...&size=512&format=png → PNG/SVG QR (qrcode)
Group E: Auth + API Keys
POST /v1/auth/register → email, password, name
POST /v1/auth/login → sets __Host-session
POST /v1/auth/logout → revoke session
GET /v1/auth/me → current user
GET /v1/api-keys → list (hash only)
POST /v1/api-keys → create (plaintext shown once)
DELETE /v1/api-keys/:id → revoke
All 5 groups are fully implemented, validated, tested, and documented in Phase 1. Remaining 145 features (see
14_PRODUCT_SPECIFICATION.md) have OpenAPI stubs + docs + permission rows but handlers return501 Not Implementedwithtype: not_implementeduntil their phase.
7. Validation
// shared schema (packages/config/src/schemas/link.ts)
export const createLinkSchema = z.object({
originalUrl: z.string().url().max(2048).refine(isHttpUrl),
customAlias: z.string().min(3).max(30).regex(/^[a-z0-9-_]+$/i).optional(),
title: z.string().max(255).optional(),
expiresAt: z.string().datetime().optional(),
password: z.string().min(8).max(128).optional(),
})
Server enforces with zod + global pipe: whitelist + forbidNonWhitelisted.
Destination URL validator: http:/https: only, no javascript:, no credentials-in-URL, no control chars, punycode normalized, SSRF guard on any server fetch (reject private/loopback).
8. OpenAPI & SDKs
- Generate:
zod-openapi→openapi.jsonat build; CI contract test: every documented route exists & validates - Docs site:
/docswith method badges, schema, examples
Runnable examples per endpoint (tested in CI against local stack):
# curl
curl -X POST https://yas.sh/api/v1/links \
-H "Authorization: Bearer yas_live_..." \
-H "Content-Type: application/json" \
-d '{"originalUrl":"https://example.com"}'
# JS fetch
await fetch("https://yas.sh/api/v1/links", { method:"POST", headers:{Authorization:"Bearer ..."}, body: JSON.stringify({originalUrl}) })
# TS SDK
import { Yas } from "@yas/sdk"; const yas = new Yas({ apiKey: "..." }); await yas.links.create({ originalUrl: "..." })
# Python / PHP / Go — same shape, in /docs
Ship @yas/sdk (TS) in packages/sdk generated from OpenAPI; others as documented snippets.
9. Discovery for AI
openapi.jsonis source of truth — SDKs, docs, tests generated from itapps/api/src/index.ts(orapps/web/app/api) auto-mountsmodules/*/api.ts- No hardcoded endpoint strings in UI — import from SDK
Next: 05_UI_UX.md — design language.