Skip to content
Y
YAS.SH
Docs

04_API_PLATFORM

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-openapi for docs
  • Spec: OpenAPI 3.1 generated from code, served at /v1/docs/openapi.json, UI at /v1/docs
  • Auth: Better Auth sessions (cookie __Host-session) + API keys (Authorization: Bearer yas_live_...)
  • Errors: RFC 9457 application/problem+json with 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-id propagated to logs
  • Rate limit headers: RateLimit-Limit, RateLimit-Remaining, Retry-After on 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 return 501 Not Implemented with type: not_implemented until 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-openapiopenapi.json at build; CI contract test: every documented route exists & validates
  • Docs site: /docs with 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.json is source of truth — SDKs, docs, tests generated from it
  • apps/api/src/index.ts (or apps/web/app/api) auto-mounts modules/*/api.ts
  • No hardcoded endpoint strings in UI — import from SDK

Next: 05_UI_UX.md — design language.

🍪 Cookies & privacy. yas.sh uses only essential cookies to keep you signed in and remember your preferences. We do not run third-party trackers. See our cookie policy and privacy policy.
Settings