{"slug":"api-design","title":"Designing a REST API","description":"A practical walkthrough of building a clean REST API, with examples in multiple languages.","markdown":"# Designing a REST API\n\nA practical walkthrough of building a clean REST API, with examples in multiple languages.\n\n## The schema\n\nWe're building a bookmarks API. Each bookmark has an ID, a URL, a title, and tags.\n\n```json\n{\n  \"id\": \"bk_7a3x9k\",\n  \"url\": \"https://example.com/article\",\n  \"title\": \"Interesting Article\",\n  \"tags\": [\"reading\", \"tech\"],\n  \"created_at\": \"2026-03-25T10:30:00Z\"\n}\n```\n\n## The server\n\nA minimal Express server in TypeScript:\n\n```typescript\nimport express from \"express\";\n\ninterface Bookmark {\n  id: string;\n  url: string;\n  title: string;\n  tags: string[];\n  created_at: string;\n}\n\nconst app = express();\napp.use(express.json());\n\nconst bookmarks = new Map<string, Bookmark>();\n\napp.get(\"/api/bookmarks\", (req, res) => {\n  const tag = req.query.tag as string | undefined;\n  let results = [...bookmarks.values()];\n\n  if (tag) {\n    results = results.filter((b) => b.tags.includes(tag));\n  }\n\n  res.json({ bookmarks: results, total: results.length });\n});\n\napp.post(\"/api/bookmarks\", (req, res) => {\n  const { url, title, tags = [] } = req.body;\n\n  if (!url || !title) {\n    return res.status(400).json({ error: \"url and title are required\" });\n  }\n\n  const bookmark: Bookmark = {\n    id: `bk_${crypto.randomUUID().slice(0, 6)}`,\n    url,\n    title,\n    tags,\n    created_at: new Date().toISOString(),\n  };\n\n  bookmarks.set(bookmark.id, bookmark);\n  res.status(201).json(bookmark);\n});\n\napp.delete(\"/api/bookmarks/:id\", (req, res) => {\n  const deleted = bookmarks.delete(req.params.id);\n  if (!deleted) return res.status(404).json({ error: \"not found\" });\n  res.status(204).end();\n});\n\napp.listen(3000, () => console.log(\"listening on :3000\"));\n```\n\n## Consuming it with Python\n\n```python\nimport requests\n\nAPI = \"http://localhost:3000/api/bookmarks\"\n\n# Create a bookmark\nresponse = requests.post(API, json={\n    \"url\": \"https://markdown.page\",\n    \"title\": \"Markdown Page\",\n    \"tags\": [\"tools\", \"writing\"]\n})\n\nbookmark = response.json()\nprint(f\"Created: {bookmark['id']} — {bookmark['title']}\")\n\n# List all bookmarks tagged 'tools'\nresponse = requests.get(API, params={\"tag\": \"tools\"})\ndata = response.json()\n\nfor b in data[\"bookmarks\"]:\n    print(f\"  [{b['id']}] {b['title']} — {b['url']}\")\n\n# Delete it\nrequests.delete(f\"{API}/{bookmark['id']}\")\nprint(f\"Deleted: {bookmark['id']}\")\n```\n\n## A Go client\n\n```go\npackage main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype Bookmark struct {\n\tID        string   `json:\"id\"`\n\tURL       string   `json:\"url\"`\n\tTitle     string   `json:\"title\"`\n\tTags      []string `json:\"tags\"`\n\tCreatedAt string   `json:\"created_at\"`\n}\n\nfunc main() {\n\tbody, _ := json.Marshal(map[string]interface{}{\n\t\t\"url\":   \"https://go.dev\",\n\t\t\"title\": \"The Go Programming Language\",\n\t\t\"tags\":  []string{\"languages\", \"go\"},\n\t})\n\n\tresp, err := http.Post(\n\t\t\"http://localhost:3000/api/bookmarks\",\n\t\t\"application/json\",\n\t\tbytes.NewReader(body),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar bm Bookmark\n\tjson.NewDecoder(resp.Body).Decode(&bm)\n\tfmt.Printf(\"Created: %s — %s\\n\", bm.ID, bm.Title)\n}\n```\n\n## Testing with curl\n\n```bash\n# Create\ncurl -s -X POST http://localhost:3000/api/bookmarks \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\":\"https://example.com\",\"title\":\"Example\",\"tags\":[\"test\"]}' \\\n  | jq .\n\n# List\ncurl -s http://localhost:3000/api/bookmarks | jq '.bookmarks[] | .title'\n\n# Filter by tag\ncurl -s \"http://localhost:3000/api/bookmarks?tag=test\" | jq .\n\n# Delete\ncurl -s -X DELETE http://localhost:3000/api/bookmarks/bk_abc123\n```\n\n## SQL schema\n\nIf you want to persist bookmarks, here's a PostgreSQL schema:\n\n```sql\nCREATE TABLE bookmarks (\n    id         TEXT PRIMARY KEY,\n    url        TEXT NOT NULL,\n    title      TEXT NOT NULL,\n    tags       TEXT[] DEFAULT '{}',\n    created_at TIMESTAMPTZ DEFAULT now()\n);\n\nCREATE INDEX idx_bookmarks_tags ON bookmarks USING GIN (tags);\n\n-- Find bookmarks by tag\nSELECT id, url, title, created_at\nFROM bookmarks\nWHERE 'tools' = ANY(tags)\nORDER BY created_at DESC\nLIMIT 20;\n```\n\n## Nginx reverse proxy\n\n```nginx\nupstream api {\n    server 127.0.0.1:3000;\n}\n\nserver {\n    listen 443 ssl;\n    server_name api.example.com;\n\n    ssl_certificate     /etc/ssl/certs/api.pem;\n    ssl_certificate_key /etc/ssl/private/api.key;\n\n    location /api/ {\n        proxy_pass http://api;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}\n```\n\n## The takeaway\n\nA good API is boring. Predictable URLs, consistent error shapes, standard HTTP methods. The code above isn't clever — it's clear. That's the point.\n"}