Build a REST API
A step-by-step tutorial: build a small JSON REST API with routes, middleware, and security using serez-http, then ship it as a Docker image with serez-apipack.
GET/POST routes, route parameters, logging and auth middleware, rate limiting, then a deployable Docker container.Step 1 — Set up
mkdir tasks-api
cd tasks-api
sz init --y
sz install serez-httpsz init --y creates a serez.json with a dev script (sz index.sz), so we'll start the server with sz run dev. Create index.sz and create the app:
import "serez-http"
const app = new App()Step 2 — Your first route
A route is an HTTP verb, a path, and a handler that receives req and res. Send JSON with res.json():
app.GET("/", fn(req, res) {
res.json({"message": "tasks API", "version": "1.0"})
})
app.listen(3000, fn() {
out "listening on http://127.0.0.1:3000"
})Run it with the dev script and hit it from another terminal:
sz run dev
# elsewhere:
curl http://127.0.0.1:3000/get and use are reserved words in Serez Code, so the methods are GET/POST/PUT/delete and middleware is registered with addMw.Step 3 — Route parameters and a data store
Use :param segments and read them from req["params"]. We'll keep tasks in a simple in-memory array:
let tasks = ["buy milk", "write docs", "ship release"]
// List all tasks
app.GET("/tasks", fn(req, res) {
res.json({"tasks": tasks, "count": tasks.length()})
})
// Get one task by index: /tasks/0
app.GET("/tasks/:id", fn(req, res) {
let id = parseInt(req["params"]["id"])
if (id < 0 || id >= tasks.length()) {
res.status(404).json({"error": "no such task"})
} else {
res.json({"id": id, "task": tasks[id]})
}
})Step 4 — Handle POST with a JSON body
The raw body is in req["body"]. Parse it with JSON.parse:
app.POST("/tasks", fn(req, res) {
let body = JSON.parse(req["body"])
let title = body["title"]
if (title == null) {
res.status(400).json({"error": "title is required"})
} else {
tasks.push(title)
res.status(201).json({"added": title, "count": tasks.length()})
}
})curl -X POST http://127.0.0.1:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "review PR"}'Step 5 — Middleware: logging and auth
Middleware runs before handlers. Register it with addMw; call next() to continue, or send a response to stop the chain. Order matters — register middleware before listen:
// Log every request
app.addMw(fn(req, res, next) {
out req["method"] + " " + req["path"]
next()
})
// Require a token on write routes
app.addMw(fn(req, res, next) {
if (req["method"] == "POST" && req["authorization"] == "") {
res.status(401).json({"error": "unauthorized"})
} else {
next()
}
})Step 6 — Security: rate limiting and CORS
Throttle abusive clients with the built-in per-IP rate limiter, and control which origins may call your API with corsMiddleware:
// Max 100 requests per minute per IP
app.addMw(fn(req, res, next) {
if (app.rateLimit(req["ip"], 100, 60)) {
next()
} else {
res.status(429).json({"error": "too many requests"})
}
})
// Allow a specific origin
app.addMw(corsMiddleware(
"https://my-frontend.com",
"GET, POST, PUT, DELETE",
"Content-Type, Authorization"
))Step 7 — Handle errors
Register a global error handler so a thrown exception in a handler returns a clean 500 instead of crashing the server:
app.error(fn(err, req, res) {
out "error: " + err
res.status(500).json({"error": "internal server error"})
})Step 8 — Deploy with serez-apipack
serez-apipack packages the project into a self-contained Docker image — the host only needs Docker, not Serez Code. Your serez.json already has a dev script from sz init; declare the dependency and the full dev/build script pair:
{
"name": "tasks-api",
"version": "1.0.0",
"main": "index.sz",
"scripts": {
"dev": "sz index.sz",
"build": "sz pack.sz"
},
"dependencies": {
"serez-http": "1.0.0",
"serez-apipack": "1.0.0"
}
}Now the workflow is two commands: sz run dev to develop locally, and sz run build to produce the Docker image. Install the packer and build:
sz install # installs serez-http + serez-apipack from serez.json
sz run dev # local development server
sz run build # → runs sz pack.sz, builds tasks-api:1.0.0
# pack.sz also takes options directly:
sz pack.sz port=8080 tag=tasks-api:latestRun the container — it's reachable from outside the host:
docker run -p 3000:3000 tasks-api:1.0.0127.0.0.1, so apipack adds a socat proxy inside the image that forwards 0.0.0.0:PORT → the app. You write a local server; apipack makes it reachable.Next steps
- Add WebSocket endpoints with
app.ws(path, fn(req, ws){})(core v4.5.0+). - See the serez-http reference and serez-apipack reference.
- Serve a model: combine this with the GPT agent tutorial to expose inference over HTTP.