simple_web

Cookbook

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