Building REST APIs#
This guide shows you how to create RESTful APIs with Bialet, including routing, CORS, and authentication. For database-specific patterns like pagination, filtering, sorting, and migrations, see the Database guide.
Quick Start#
Create an API endpoint by making a .wren file in your project:
// api/users.wren - Handles /api/users
// Enable CORS for browser access
if (Response.cors) return
// Get ID from query parameter (e.g., /api/users?id=1)
var id = Request.get("id")
if (id) {
// Get single user
var user = `SELECT id, name, email FROM users WHERE id = ?`.first([id])
if (!user) {
Response.status(404)
return Response.json({"error": "User not found"})
}
return Response.json(user)
}
// List all users
var users = `SELECT id, name, email FROM users`.fetch()
Response.json(users)
Visit http://localhost:7001/api/users to list all users, or http://localhost:7001/api/users?id=1 to get a specific user.
Note: The backtick
`query syntax and?placeholders are covered in the Query Object section of the Database guide. Pagination, filtering, and sorting patterns are covered there as well.
Project Structure#
For a typical API, organize your files like this:
my-api/
├── _db.sqlite3 # Database file
├── _migration.wren # Database migrations
├── api/
│ ├── users.wren # /api/users endpoint
│ └── products.wren # /api/products endpoint
└── _app.wren # Shared utilities (optional)
Handling Different HTTP Methods#
Handle different HTTP methods in a single file using query parameters:
// api/users.wren - Handles /api/users
// Enable CORS for all methods
if (Response.cors) return
// Get ID from query parameter
var id = Request.get("id")
// GET /api/users?id=1 - Get a single user
if (id && Request.method == "GET") {
var user = `SELECT id, name, email, created_at FROM users WHERE id = ?`.first([id])
if (!user) {
Response.status(404)
return Response.json({"error": "User not found"})
}
return Response.json(user)
}
// GET /api/users - List all users
if (!id && Request.method == "GET") {
var users = `SELECT id, name, email, created_at FROM users`.fetch()
return Response.json(users)
}
// POST /api/users - Create a new user
if (Request.method == "POST") {
var data = Request.json()
var name = data["name"]
var email = data["email"]
// Validation
if (!name || !email) {
Response.status(400)
return Response.json({"error": "Name and email are required"})
}
// Insert new user
var userId = `INSERT INTO users (name, email) VALUES (?, ?)`.query([name, email])
Response.status(201)
return Response.json({"id": userId, "name": name, "email": email})
}
// PUT /api/users?id=1 - Update a user
if (id && Request.method == "PUT") {
var data = Request.json()
var name = data["name"]
var email = data["email"]
// Check if user exists
var user = `SELECT id FROM users WHERE id = ?`.first([id])
if (!user) {
Response.status(404)
return Response.json({"error": "User not found"})
}
// Update user
`UPDATE users SET name = ?, email = ? WHERE id = ?`.query([name, email, id])
return Response.json({"id": id, "name": name, "email": email})
}
// DELETE /api/users?id=1 - Delete a user
if (id && Request.method == "DELETE") {
`DELETE FROM users WHERE id = ?`.query([id])
Response.status(204)
return
}
// 404 for unknown operations
Response.status(404)
Response.json({"error": "Invalid request"})
This single-file pattern — one .wren file handling GET, POST, PUT, and DELETE — is the idiomatic Bialet approach for API endpoints. No _route.wren needed: the query string carries the dynamic data while the HTTP method determines the action. For complete REST APIs that need pagination and sorting, combine this pattern with the techniques from the Database guide.
CORS Configuration#
Enable Cross-Origin Resource Sharing to allow browser access from different domains:
// Allow all origins (simplest form)
if (Response.cors) return
// Allow specific origin
if (Response.cors("https://myapp.com")) return
// Full control over CORS
if (Response.cors("https://myapp.com", "GET, POST, PUT, DELETE", "Content-Type, Authorization")) return
The Response.cors method:
Sets appropriate CORS headers
Automatically handles OPTIONS preflight requests
Returns
truefor OPTIONS (so you return early)Returns
falsefor other methods (so processing continues)
Request Parsing#
JSON Request Body#
// Using the convenience method
var data = Request.json()
var name = data["name"]
var email = data["email"]
// Or check content type first
if (Request.isJson) {
var data = Request.json()
// ... use the data
}
Form Data#
⚠️ CRITICAL:
Request.post()returnsnullwhen the key is missing. ALWAYS provide a fallback or null check — never call string methods on the raw result.
// `||` works because `null` is falsey in Wren;
// if an empty string ("") is valid input, use a separate `if` check instead.
var name = Request.post("name") || ""
var email = Request.post("email") || ""
Query Parameters#
// GET /api/users?page=2&limit=10
var page = Request.get("page")
var limit = Request.get("limit")
Multiple Query Parameters#
// For /api/users?id=123&include=posts
var userId = Request.get("id")
var include = Request.get("include")
Response Formats#
JSON Response#
Response.json({"message": "Success", "data": users})
Status Codes#
Response.status(200) // OK
Response.status(201) // Created
Response.status(204) // No Content
Response.status(400) // Bad Request
Response.status(401) // Unauthorized
Response.status(403) // Forbidden
Response.status(404) // Not Found
Response.status(500) // Internal Server Error
Error Responses#
Response.status(400)
Response.json({
"error": "Validation failed",
"details": {
"name": "Name is required",
"email": "Invalid email format"
}
})
Authentication#
API Key Authentication#
// api/users.wren
// Enable CORS
if (Response.cors) return
// Check API key
var apiKey = Request.header("x-api-key")
if (!apiKey) {
Response.status(401)
return Response.json({"error": "API key is required"})
}
// Validate API key
var user = `SELECT * FROM api_keys WHERE key = ? AND active = 1`.first([apiKey])
if (!user) {
Response.status(401)
return Response.json({"error": "Invalid API key"})
}
// Continue with your API logic...
var id = Request.get("id")
Bearer Token Authentication#
// Get authorization header
var authHeader = Request.header("authorization")
if (!authHeader) {
Response.status(401)
return Response.json({"error": "Authorization required"})
}
// Extract token (format: "Bearer <token>")
var parts = authHeader.split(" ")
if (parts.count != 2 || parts[0] != "Bearer") {
Response.status(401)
return Response.json({"error": "Invalid authorization format"})
}
var token = parts[1]
// Validate token
var session = `SELECT * FROM sessions WHERE token = ? AND expires_at > datetime('now')`.first([token])
if (!session) {
Response.status(401)
return Response.json({"error": "Invalid or expired token"})
}
Complete Example: Products API#
Here’s a complete REST API for managing products. This example brings together CORS, authentication, request parsing, and CRUD operations covered in the sections above. The pagination logic uses the LIMIT/OFFSET pattern described in the Database guide — combine it with the safe sorting techniques there for sortable, paginated lists.
// api/products.wren
// Enable CORS
if (Response.cors) return
// Authentication (optional)
var apiKey = Request.header("x-api-key")
if (!apiKey) {
Response.status(401)
return Response.json({"error": "API key required"})
}
var user = `SELECT * FROM api_keys WHERE key = ? AND active = 1`.first([apiKey])
if (!user) {
Response.status(401)
return Response.json({"error": "Invalid API key"})
}
var id = Request.get("id")
// GET /api/products?id=1 - Get single product
if (id && Request.method == "GET") {
var product = `SELECT * FROM products WHERE id = ?`.first([id])
if (!product) {
Response.status(404)
return Response.json({"error": "Product not found"})
}
return Response.json(product)
}
// GET /api/products - List products with pagination
if (!id && Request.method == "GET") {
var page = Util.toNum(Request.get("page") ? Request.get("page") : "1")
var limit = Util.toNum(Request.get("limit") ? Request.get("limit") : "20")
var offset = (page - 1) * limit
var total = `SELECT COUNT(*) as count FROM products`.first()["count"]
var products = `SELECT * FROM products ORDER BY created_at DESC LIMIT ? OFFSET ?`.fetch([limit, offset])
return Response.json({
"data": products,
"pagination": {
"page": page,
"limit": limit,
"total": Util.toNum(total),
"pages": ((Util.toNum(total) + limit - 1) / limit).floor
}
})
}
// POST /api/products - Create product
if (Request.method == "POST") {
var data = Request.json()
var name = data["name"]
var description = data["description"]
var price = data["price"]
var stock = data["stock"] || 0
// Validation
if (!name || !price) {
Response.status(400)
return Response.json({"error": "Name and price are required"})
}
var productId = `
INSERT INTO products (name, description, price, stock)
VALUES (?, ?, ?, ?)
`.query([name, description, price, stock])
Response.status(201)
return Response.json({
"id": productId,
"name": name,
"description": description,
"price": price,
"stock": stock
})
}
// PUT /api/products?id=1 - Update product
if (id && Request.method == "PUT") {
var product = `SELECT * FROM products WHERE id = ?`.first([id])
if (!product) {
Response.status(404)
return Response.json({"error": "Product not found"})
}
var data = Request.json()
var name = data["name"] || product["name"]
var description = data["description"] || product["description"]
var price = data["price"] || product["price"]
var stock = data["stock"] || product["stock"]
`
UPDATE products
SET name = ?, description = ?, price = ?, stock = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`.query([name, description, price, stock, id])
return Response.json({
"id": id,
"name": name,
"description": description,
"price": price,
"stock": stock
})
}
// DELETE /api/products?id=1 - Delete product
if (id && Request.method == "DELETE") {
var product = `SELECT id FROM products WHERE id = ?`.first([id])
if (!product) {
Response.status(404)
return Response.json({"error": "Product not found"})
}
`DELETE FROM products WHERE id = ?`.query([id])
Response.status(204)
return
}
// 404 for unknown operations
Response.status(404)
Response.json({"error": "Invalid request"})
Testing Your API#
Using curl#
# List products
curl http://localhost:7001/api/products
# Get single product
curl "http://localhost:7001/api/products?id=1"
# Create product
curl -X POST http://localhost:7001/api/products \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"name":"Widget","price":19.99,"stock":100}'
# Update product
curl -X PUT "http://localhost:7001/api/products?id=1" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"price":24.99}'
# Delete product
curl -X DELETE "http://localhost:7001/api/products?id=1"
# With pagination
curl "http://localhost:7001/api/products?page=2&limit=10"
Using JavaScript (fetch)#
// List products
const products = await fetch('http://localhost:7001/api/products')
.then(res => res.json());
// Get single product
const product = await fetch('http://localhost:7001/api/products?id=1')
.then(res => res.json());
// Create product
const newProduct = await fetch('http://localhost:7001/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
name: 'Widget',
price: 19.99,
stock: 100
})
}).then(res => res.json());
// Update product
const updated = await fetch('http://localhost:7001/api/products?id=1', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
price: 24.99
})
}).then(res => res.json());
// Delete product
await fetch('http://localhost:7001/api/products?id=1', {
method: 'DELETE',
headers: {
'X-API-Key': 'your-api-key'
}
});
Next Steps#
Learn about Database operations — the Query object, pagination, sorting, inserts, updates, and migrations
Set up database migrations to define your API’s schema
Explore Session management
Read about File uploads
Understand Advanced Routing — when to use query parameters vs
_route.wren