Cookbook Recipes
Real-world examples and patterns for HTTP client and server development.
Recipe 1: Complete REST API
Build a full CRUD REST API with JSON responses.
class
USER_API
create
make
feature {NONE}
make
do
create json
create users.make (10)
create server.make (8080)
-- Middleware
server.use (create {SIMPLE_WEB_LOGGING_MIDDLEWARE}.make)
server.use (create {SIMPLE_WEB_CORS_MIDDLEWARE}.make)
-- Routes
server.on_get ("/api/users", agent list_users)
server.on_get ("/api/users/{id}", agent get_user)
server.on_post ("/api/users", agent create_user)
server.on_put ("/api/users/{id}", agent update_user)
server.on_delete ("/api/users/{id}", agent delete_user)
print ("Server starting on port 8080...%N")
server.start
end
feature -- Handlers
list_users (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
local
arr: SIMPLE_JSON_ARRAY
do
arr := json.new_array
across users as u loop
arr.add_object (user_to_json (u))
end
res.send_json (arr.to_json)
end
get_user (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
do
if attached req.path_parameter ("id") as id_str then
if id_str.is_integer and then users.has (id_str.to_integer) then
res.send_json_object (user_to_json (users.item (id_str.to_integer)))
else
res.set_status (404)
res.send_json ('{"error": "User not found"}')
end
end
end
create_user (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
local
new_id: INTEGER
name, email: STRING
do
if attached json.parse (req.body) as v and then v.is_object then
if attached v.as_object.string_item ("name") as n then
name := n
email := ""
if attached v.as_object.string_item ("email") as e then
email := e
end
new_id := users.count + 1
users.put ([new_id, name, email], new_id)
res.set_status (201)
res.send_json_object (json.new_object
.put_integer ("id", new_id)
.put_string ("name", name)
.put_string ("email", email))
else
res.set_status (400)
res.send_json ('{"error": "Name required"}')
end
else
res.set_status (400)
res.send_json ('{"error": "Invalid JSON"}')
end
end
update_user (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
do
-- Similar to create_user with id lookup
end
delete_user (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
do
if attached req.path_parameter ("id") as id_str then
if id_str.is_integer and then users.has (id_str.to_integer) then
users.remove (id_str.to_integer)
res.set_status (204) -- No Content
else
res.set_status (404)
res.send_json ('{"error": "User not found"}')
end
end
end
feature {NONE} -- Implementation
server: SIMPLE_WEB_SERVER
json: SIMPLE_JSON
users: HASH_TABLE [TUPLE [id: INTEGER; name, email: STRING], INTEGER]
user_to_json (u: TUPLE [id: INTEGER; name, email: STRING]): SIMPLE_JSON_OBJECT
do
Result := json.new_object
.put_integer ("id", u.id)
.put_string ("name", u.name)
.put_string ("email", u.email)
end
end
Recipe 2: Local AI Chatbot
Build a command-line chatbot using Ollama.
class
CHATBOT
create
make
feature {NONE}
make
local
input: STRING
do
create ollama
create json
create history.make (10)
print ("Chatbot ready! (type 'quit' to exit)%N")
print ("Using model: " + model + "%N%N")
from
until
False
loop
io.put_string ("You: ")
io.read_line
input := io.last_string.twin
if input.same_string ("quit") then
print ("%NGoodbye!%N")
break
end
-- Add user message to history
history.force (["user", input])
-- Get AI response
if attached get_response as response then
print ("Bot: " + response + "%N%N")
history.force (["assistant", response])
else
print ("[Error getting response]%N%N")
end
end
end
feature {NONE}
ollama: SIMPLE_WEB_OLLAMA_CLIENT
json: SIMPLE_JSON
history: ARRAYED_LIST [TUPLE [role, content: STRING]]
model: STRING = "llama3"
get_response: detachable STRING
local
messages: ARRAY [TUPLE [role, content: STRING]]
response: SIMPLE_WEB_RESPONSE
i: INTEGER
do
-- Convert history to array
create messages.make_filled (["", ""], 1, history.count)
from i := 1 until i > history.count loop
messages[i] := history[i]
i := i + 1
end
response := ollama.chat (model, messages)
if response.is_success then
-- Extract response from JSON
if attached json.parse (response.body) as v then
Result := json.query_string (v, "$.message.content")
end
end
end
end
Recipe 3: External API Client
Create a typed client for an external API.
class
WEATHER_CLIENT
create
make
feature {NONE}
make (a_api_key: STRING)
do
api_key := a_api_key
create client.make
create json
end
feature -- Weather Operations
current_weather (a_city: STRING): detachable WEATHER_DATA
-- Get current weather for city
local
url: STRING
response: SIMPLE_WEB_RESPONSE
do
url := base_url + "/current.json?key=" + api_key + "&q=" + a_city
response := client.get (url)
if response.is_success then
Result := parse_weather (response.body)
end
end
forecast (a_city: STRING; a_days: INTEGER): ARRAYED_LIST [WEATHER_DATA]
-- Get forecast for city
local
url: STRING
response: SIMPLE_WEB_RESPONSE
do
create Result.make (a_days)
url := base_url + "/forecast.json?key=" + api_key
+ "&q=" + a_city + "&days=" + a_days.out
response := client.get (url)
if response.is_success then
Result := parse_forecast (response.body)
end
end
feature {NONE} -- Implementation
client: SIMPLE_WEB_CLIENT
json: SIMPLE_JSON
api_key: STRING
base_url: STRING = "https://api.weatherapi.com/v1"
parse_weather (a_json: STRING): detachable WEATHER_DATA
do
if attached json.parse (a_json) as v then
create Result.make
Result.set_location (json.query_string (v, "$.location.name"))
Result.set_temp_c (json.query_integer (v, "$.current.temp_c").to_real)
Result.set_condition (json.query_string (v, "$.current.condition.text"))
end
end
end
Recipe 4: API with JWT Authentication
Secure API with JWT tokens.
class
SECURE_API
create
make
feature {NONE}
make
do
create server.make (8080)
-- Public routes (no auth)
server.on_post ("/auth/login", agent handle_login)
server.on_post ("/auth/register", agent handle_register)
-- Protected routes (auth required)
server.use (create {SIMPLE_WEB_AUTH_MIDDLEWARE}.make_bearer (jwt_secret))
server.on_get ("/api/profile", agent handle_profile)
server.on_get ("/api/data", agent handle_data)
server.start
end
feature -- Auth Handlers
handle_login (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
local
username, password, token: STRING
do
if attached json.parse (req.body) as v and then v.is_object then
if attached v.as_object.string_item ("username") as u and
attached v.as_object.string_item ("password") as p then
if validate_credentials (u, p) then
token := generate_jwt (u)
res.send_json_object (json.new_object
.put_string ("token", token)
.put_integer ("expires_in", 3600))
else
res.set_status (401)
res.send_json ('{"error": "Invalid credentials"}')
end
else
res.set_status (400)
res.send_json ('{"error": "Username and password required"}')
end
end
end
handle_profile (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
-- This handler only reached if auth passes
do
res.send_json ('{"username": "authenticated_user", "role": "user"}')
end
feature {NONE}
jwt_secret: STRING = "your-256-bit-secret"
json: SIMPLE_JSON
server: SIMPLE_WEB_SERVER
validate_credentials (u, p: STRING): BOOLEAN
-- In real app, check database
do
Result := u.same_string ("admin") and p.same_string ("secret")
end
generate_jwt (username: STRING): STRING
-- Generate JWT token (simplified)
do
-- Use simple_jwt library for real implementation
Result := "eyJ..."
end
end
Recipe 5: Webhook Receiver
Handle incoming webhooks from external services.
class
WEBHOOK_RECEIVER
create
make
feature {NONE}
make (a_secret: STRING)
do
webhook_secret := a_secret
create json
create server.make (9000)
server.on_post ("/webhook/github", agent handle_github)
server.on_post ("/webhook/stripe", agent handle_stripe)
server.start
end
feature -- Handlers
handle_github (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
local
event_type: STRING
do
-- Verify signature
if not verify_github_signature (req) then
res.set_status (401)
res.send_json ('{"error": "Invalid signature"}')
return
end
-- Get event type from header
if attached req.header ("X-GitHub-Event") as evt then
event_type := evt
if event_type.same_string ("push") then
handle_push_event (req.body)
elseif event_type.same_string ("pull_request") then
handle_pr_event (req.body)
end
end
res.send_json ('{"received": true}')
end
handle_stripe (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
do
if attached json.parse (req.body) as v then
if attached json.query_string (v, "$.type") as event_type then
if event_type.same_string ("payment_intent.succeeded") then
-- Handle successful payment
print ("Payment received!%N")
elseif event_type.same_string ("customer.subscription.deleted") then
-- Handle subscription cancellation
end
end
end
res.send_json ('{"received": true}')
end
feature {NONE}
webhook_secret: STRING
json: SIMPLE_JSON
server: SIMPLE_WEB_SERVER
verify_github_signature (req: SIMPLE_WEB_SERVER_REQUEST): BOOLEAN
-- Verify HMAC-SHA256 signature
do
-- Use simple_encryption for real implementation
Result := True
end
handle_push_event (body: STRING)
do
print ("Push event received%N")
end
handle_pr_event (body: STRING)
do
print ("PR event received%N")
end
end
Recipe 6: Simple API Proxy
Proxy requests to another service with modifications.
class
API_PROXY
create
make
feature {NONE}
make
do
create client.make
create server.make (3000)
server.use (create {SIMPLE_WEB_CORS_MIDDLEWARE}.make)
server.on_get ("/proxy/*", agent proxy_get)
server.on_post ("/proxy/*", agent proxy_post)
server.start
end
feature
proxy_get (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
local
target_url: STRING
response: SIMPLE_WEB_RESPONSE
request: SIMPLE_WEB_REQUEST
do
-- Transform /proxy/users -> https://api.example.com/users
target_url := target_base + req.path.substring (7, req.path.count)
create request.make_get (target_url)
-- Forward relevant headers
if attached req.header ("Authorization") as auth then
request.with_header ("Authorization", auth).do_nothing
end
-- Add API key for target service
request.with_header ("X-API-Key", target_api_key).do_nothing
response := client.execute (request)
-- Forward response
res.set_status (response.status_code)
res.send_json (response.body)
end
feature {NONE}
client: SIMPLE_WEB_CLIENT
server: SIMPLE_WEB_SERVER
target_base: STRING = "https://api.example.com"
target_api_key: STRING = "secret-key"
end
Recipe 7: File Upload Handler
Handle multipart file uploads.
class
FILE_UPLOAD_API
create
make
feature {NONE}
make
do
create server.make (8080)
create sanitizer
server.on_post ("/upload", agent handle_upload)
server.on_get ("/files/{filename}", agent serve_file)
server.start
end
feature
handle_upload (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
local
filename: STRING
file: PLAIN_TEXT_FILE
do
-- In real implementation, parse multipart body
-- This is simplified
if attached req.header ("X-Filename") as fn then
-- Sanitize filename
filename := sanitizer.sanitize_path (fn)
if sanitizer.is_safe_path (filename) then
create file.make_create_read_write (upload_dir + filename)
file.put_string (req.body)
file.close
res.set_status (201)
res.send_json ('{"filename": "' + filename + '"}')
else
res.set_status (400)
res.send_json ('{"error": "Invalid filename"}')
end
end
end
serve_file (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE)
do
if attached req.path_parameter ("filename") as fn then
if sanitizer.is_safe_path (fn) then
res.send_file (upload_dir + fn)
else
res.set_status (400)
res.send_json ('{"error": "Invalid path"}')
end
end
end
feature {NONE}
server: SIMPLE_WEB_SERVER
sanitizer: SIMPLE_WEB_INPUT_SANITIZER
upload_dir: STRING = "/var/uploads/"
end
Recipe 8: Rate-Limited API
Add rate limiting to your API.
class
RATE_LIMIT_MIDDLEWARE
inherit
SIMPLE_WEB_MIDDLEWARE
create
make
feature {NONE}
make (a_max_requests: INTEGER; a_window_seconds: INTEGER)
do
max_requests := a_max_requests
window_seconds := a_window_seconds
create request_counts.make (100)
end
feature
process (req: SIMPLE_WEB_SERVER_REQUEST; res: SIMPLE_WEB_SERVER_RESPONSE): BOOLEAN
local
client_ip: STRING
count: INTEGER
do
-- Get client IP (simplified)
if attached req.header ("X-Forwarded-For") as ip then
client_ip := ip
else
client_ip := "unknown"
end
-- Check rate limit
if request_counts.has (client_ip) then
count := request_counts.item (client_ip) + 1
else
count := 1
end
if count > max_requests then
res.set_status (429) -- Too Many Requests
res.set_header ("Retry-After", window_seconds.out)
res.send_json ('{"error": "Rate limit exceeded"}')
Result := False
else
request_counts.force (count, client_ip)
res.set_header ("X-RateLimit-Remaining", (max_requests - count).out)
Result := True
end
end
feature {NONE}
max_requests: INTEGER
window_seconds: INTEGER
request_counts: HASH_TABLE [INTEGER, STRING]
end
-- Usage
server.use (create {RATE_LIMIT_MIDDLEWARE}.make (100, 60)) -- 100 req/min