Building REST APIs#
This guide shows you how to create RESTful APIs with Bialet, including routing, CORS, and authentication.
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.
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") {
var result = `DELETE FROM users WHERE id = ?`.query([id])
Response.status(204)
return Response.json({})
}
// 404 for unknown operations
Response.status(404)
Response.json({"error": "Invalid request"})
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#
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/_route.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"})
}
Pagination#
// GET /api/users?page=1&limit=20
var page = Util.toNum(Request.get("page")) || 1
var limit = Util.toNum(Request.get("limit")) || 20
var offset = (page - 1) * limit
// Get total count
var total = `SELECT COUNT(*) as count FROM users`.first()["count"]
// Get paginated results
var users = `SELECT id, name, email FROM users LIMIT ? OFFSET ?`.fetch([limit, offset])
// Return with pagination metadata
Response.json({
"data": users,
"pagination": {
"page": page,
"limit": limit,
"total": Util.toNum(total),
"pages": ((Util.toNum(total) + limit - 1) / limit).floor
}
})
Filtering and Sorting#
// GET /api/users?search=john&status=active&sort=name&order=asc
var search = Request.get("search") || ""
var status = Request.get("status") || ""
var sort = Request.get("sort") || "id"
var order = Request.get("order") || "asc"
// Define allowed columns for sorting
var allowedSorts = ["id", "name", "email", "created_at"]
// Use parameterized queries with conditional logic and safe sorting
var users = `
SELECT * FROM users
WHERE (? = '' OR name LIKE '%' || ? || '%')
AND (? = '' OR status = ?)
`.order(sort, order, allowedSorts).fetch([search, search, status, status])
Response.json(users)
The .order() method validates the sort column against the allowed list and normalizes the direction (case-insensitive “asc” or “desc”), preventing SQL injection through column name manipulation. You can also pass an optional fourth parameter to add a LIMIT clause:
// Top 10 users by score
var topUsers = `SELECT * FROM users`
.order("score", "desc", ["score", "name"], 10)
.fetch
Database Migrations#
Create a _migration.wren file for your API database:
// _migration.wren
Db.migrate("001_create_users", `
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
`)
Db.migrate("002_create_api_keys", `
CREATE TABLE api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
user_id INTEGER NOT NULL,
active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX idx_api_keys_key ON api_keys(key);
`)
Db.migrate("003_create_products", `
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
price REAL NOT NULL,
stock INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`)
Complete Example: Products API#
Here’s a complete REST API for managing products:
// api/_route.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")) || 1
var limit = Util.toNum(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 Response.json({})
}
// 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
Explore Session management
Read about File uploads
Check out External imports for using external libraries