Designing a REST API
A practical walkthrough of building a clean REST API, with examples in multiple languages.
The schema
We're building a bookmarks API. Each bookmark has an ID, a URL, a title, and tags.
{
"id": "bk_7a3x9k",
"url": "https://example.com/article",
"title": "Interesting Article",
"tags": ["reading", "tech"],
"created_at": "2026-03-25T10:30:00Z"
}
The server
A minimal Express server in TypeScript:
import express from "express";
interface Bookmark {
id: string;
url: string;
title: string;
tags: string[];
created_at: string;
}
const app = express();
app.use(express.json());
const bookmarks = new Map<string, Bookmark>();
app.get("/api/bookmarks", (req, res) => {
const tag = req.query.tag as string | undefined;
let results = [...bookmarks.values()];
if (tag) {
results = results.filter((b) => b.tags.includes(tag));
}
res.json({ bookmarks: results, total: results.length });
});
app.post("/api/bookmarks", (req, res) => {
const { url, title, tags = [] } = req.body;
if (!url || !title) {
return res.status(400).json({ error: "url and title are required" });
}
const bookmark: Bookmark = {
id: `bk_${crypto.randomUUID().slice(0, 6)}`,
url,
title,
tags,
created_at: new Date().toISOString(),
};
bookmarks.set(bookmark.id, bookmark);
res.status(201).json(bookmark);
});
app.delete("/api/bookmarks/:id", (req, res) => {
const deleted = bookmarks.delete(req.params.id);
if (!deleted) return res.status(404).json({ error: "not found" });
res.status(204).end();
});
app.listen(3000, () => console.log("listening on :3000"));
Consuming it with Python
import requests
API = "http://localhost:3000/api/bookmarks"
# Create a bookmark
response = requests.post(API, json={
"url": "https://markdown.page",
"title": "Markdown Page",
"tags": ["tools", "writing"]
})
bookmark = response.json()
print(f"Created: {bookmark['id']} — {bookmark['title']}")
# List all bookmarks tagged 'tools'
response = requests.get(API, params={"tag": "tools"})
data = response.json()
for b in data["bookmarks"]:
print(f" [{b['id']}] {b['title']} — {b['url']}")
# Delete it
requests.delete(f"{API}/{bookmark['id']}")
print(f"Deleted: {bookmark['id']}")
A Go client
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type Bookmark struct {
ID string `json:"id"`
URL string `json:"url"`
Title string `json:"title"`
Tags []string `json:"tags"`
CreatedAt string `json:"created_at"`
}
func main() {
body, _ := json.Marshal(map[string]interface{}{
"url": "https://go.dev",
"title": "The Go Programming Language",
"tags": []string{"languages", "go"},
})
resp, err := http.Post(
"http://localhost:3000/api/bookmarks",
"application/json",
bytes.NewReader(body),
)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var bm Bookmark
json.NewDecoder(resp.Body).Decode(&bm)
fmt.Printf("Created: %s — %s\n", bm.ID, bm.Title)
}
Testing with curl
# Create
curl -s -X POST http://localhost:3000/api/bookmarks \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","title":"Example","tags":["test"]}' \
| jq .
# List
curl -s http://localhost:3000/api/bookmarks | jq '.bookmarks[] | .title'
# Filter by tag
curl -s "http://localhost:3000/api/bookmarks?tag=test" | jq .
# Delete
curl -s -X DELETE http://localhost:3000/api/bookmarks/bk_abc123
SQL schema
If you want to persist bookmarks, here's a PostgreSQL schema:
CREATE TABLE bookmarks (
id TEXT PRIMARY KEY,
url TEXT NOT NULL,
title TEXT NOT NULL,
tags TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_bookmarks_tags ON bookmarks USING GIN (tags);
-- Find bookmarks by tag
SELECT id, url, title, created_at
FROM bookmarks
WHERE 'tools' = ANY(tags)
ORDER BY created_at DESC
LIMIT 20;
Nginx reverse proxy
upstream api {
server 127.0.0.1:3000;
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/api.pem;
ssl_certificate_key /etc/ssl/private/api.key;
location /api/ {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
The takeaway
A 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.