Skip to content

JSON API

SØAD provides excellent support for building RESTful JSON APIs through its flexible transaction system and built-in JSON rendering utilities. This recipe demonstrates how to create modern, standards-compliant APIs with proper error handling, authentication, pagination, and CORS support.


Basic RESTful API

product_api.py
from utils import render, Log
from models import Product
import json

class Product_api(object):
    def list(self, ctx):
        """GET /api/products - List all products"""
        try:
            request = ctx.getRequest()

            # Parse query parameters
            page = int(request.getParameter("page") or "1")
            limit = int(request.getParameter("limit") or "10")
            category = request.getParameter("category")
            search = request.getParameter("search")

            # Validate pagination parameters
            if page < 1: page = 1
            if limit < 1 or limit > 100: limit = 10

            # Build query
            query = "1=1"
            params = []

            if category:
                query += " AND category = ?"
                params.append(category)

            if search:
                query += " AND (name LIKE ? OR description LIKE ?)"
                params.extend(["%%%s%%" % search, "%%%s%%" % search])

            # Get total count for pagination
            total_count = Product.count(query, *params)

            # Calculate pagination
            offset = (page - 1) * limit
            total_pages = (total_count + limit - 1) // limit

            # Fetch products
            products = Product.where(query, *params).offset(offset).limit(limit).orderBy("created_at DESC")

            # Format response
            product_list = []
            for product in products:
                product_list.append({
                    "id": product.get("id"),
                    "name": product.get("name"),
                    "description": product.get("description"),
                    "price": float(product.get("price") or 0),
                    "category": product.get("category"),
                    "stock_quantity": product.get("stock_quantity"),
                    "is_active": product.getBoolean("is_active"),
                    "created_at": str(product.get("created_at")),
                    "updated_at": str(product.get("updated_at"))
                })

            response = {
                "success": True,
                "data": product_list,
                "pagination": {
                    "page": page,
                    "limit": limit,
                    "total_count": total_count,
                    "total_pages": total_pages,
                    "has_next": page < total_pages,
                    "has_prev": page > 1
                }
            }

            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in product list API: %s" % str(e))
            self._error_response(ctx, "Internal server error", 500)

    def get(self, ctx):
        """GET /api/products/{id} - Get single product"""
        try:
            request = ctx.getRequest()
            product_id = request.getParameter("id")

            if not product_id:
                return self._error_response(ctx, "Product ID is required", 400)

            product = Product.findById(product_id)

            if not product:
                return self._error_response(ctx, "Product not found", 404)

            response = {
                "success": True,
                "data": {
                    "id": product.get("id"),
                    "name": product.get("name"),
                    "description": product.get("description"),
                    "price": float(product.get("price") or 0),
                    "category": product.get("category"),
                    "stock_quantity": product.get("stock_quantity"),
                    "is_active": product.getBoolean("is_active"),
                    "created_at": str(product.get("created_at")),
                    "updated_at": str(product.get("updated_at"))
                }
            }

            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in product get API: %s" % str(e))
            self._error_response(ctx, "Internal server error", 500)

    def create(self, ctx):
        """POST /api/products - Create new product"""
        try:
            request = ctx.getRequest()

            # Extract and validate required fields
            name = request.getParameter("name")
            price = request.getParameter("price")
            category = request.getParameter("category")

            if not name or len(name.strip()) < 2:
                return self._error_response(ctx, "Product name is required (min 2 characters)", 400)

            if not price:
                return self._error_response(ctx, "Price is required", 400)

            try:
                price_float = float(price)
                if price_float < 0:
                    return self._error_response(ctx, "Price must be positive", 400)
            except ValueError:
                return self._error_response(ctx, "Invalid price format", 400)

            if not category:
                return self._error_response(ctx, "Category is required", 400)

            # Create product
            product = Product()
            product.set("name", name.strip())
            product.set("description", request.getParameter("description") or "")
            product.set("price", price_float)
            product.set("category", category)
            product.set("stock_quantity", int(request.getParameter("stock_quantity") or "0"))
            product.set("is_active", request.getParameter("is_active") == "true")
            product.save()

            response = {
                "success": True,
                "message": "Product created successfully",
                "data": {
                    "id": product.get("id"),
                    "name": product.get("name"),
                    "description": product.get("description"),
                    "price": float(product.get("price")),
                    "category": product.get("category"),
                    "stock_quantity": product.get("stock_quantity"),
                    "is_active": product.getBoolean("is_active"),
                    "created_at": str(product.get("created_at"))
                }
            }

            ctx.getResponse().setStatus(201)  # Created
            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in product create API: %s" % str(e))
            self._error_response(ctx, "Internal server error", 500)

    def update(self, ctx):
        """PUT /api/products/{id} - Update product"""
        try:
            request = ctx.getRequest()
            product_id = request.getParameter("id")

            if not product_id:
                return self._error_response(ctx, "Product ID is required", 400)

            product = Product.findById(product_id)
            if not product:
                return self._error_response(ctx, "Product not found", 404)

            # Update fields if provided
            name = request.getParameter("name")
            if name:
                if len(name.strip()) < 2:
                    return self._error_response(ctx, "Product name must be at least 2 characters", 400)
                product.set("name", name.strip())

            price = request.getParameter("price")
            if price:
                try:
                    price_float = float(price)
                    if price_float < 0:
                        return self._error_response(ctx, "Price must be positive", 400)
                    product.set("price", price_float)
                except ValueError:
                    return self._error_response(ctx, "Invalid price format", 400)

            description = request.getParameter("description")
            if description is not None:
                product.set("description", description)

            category = request.getParameter("category")
            if category:
                product.set("category", category)

            stock_quantity = request.getParameter("stock_quantity")
            if stock_quantity is not None:
                try:
                    product.set("stock_quantity", int(stock_quantity))
                except ValueError:
                    return self._error_response(ctx, "Invalid stock quantity", 400)

            is_active = request.getParameter("is_active")
            if is_active is not None:
                product.set("is_active", is_active == "true")

            product.save()

            response = {
                "success": True,
                "message": "Product updated successfully",
                "data": {
                    "id": product.get("id"),
                    "name": product.get("name"),
                    "description": product.get("description"),
                    "price": float(product.get("price")),
                    "category": product.get("category"),
                    "stock_quantity": product.get("stock_quantity"),
                    "is_active": product.getBoolean("is_active"),
                    "updated_at": str(product.get("updated_at"))
                }
            }

            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in product update API: %s" % str(e))
            self._error_response(ctx, "Internal server error", 500)

    def delete(self, ctx):
        """DELETE /api/products/{id} - Delete product"""
        try:
            request = ctx.getRequest()
            product_id = request.getParameter("id")

            if not product_id:
                return self._error_response(ctx, "Product ID is required", 400)

            product = Product.findById(product_id)
            if not product:
                return self._error_response(ctx, "Product not found", 404)

            product.delete()

            response = {
                "success": True,
                "message": "Product deleted successfully"
            }

            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in product delete API: %s" % str(e))
            self._error_response(ctx, "Internal server error", 500)

    def _error_response(self, ctx, message, status_code=400):
        """Helper method to generate error responses"""
        response = {
            "success": False,
            "error": {
                "message": message,
                "code": status_code
            }
        }
        ctx.getResponse().setStatus(status_code)
        ctx.go_to = render.as_json(ctx, response)
CREATE TABLE product (
    id int NOT NULL AUTO_INCREMENT,
    name varchar(200) NOT NULL,
    description text,
    price decimal(10,2) NOT NULL,
    category varchar(50) NOT NULL,
    stock_quantity int DEFAULT 0,
    is_active boolean DEFAULT true,
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_category (category),
    KEY idx_active (is_active),
    KEY idx_name (name),
    FULLTEXT KEY idx_search (name, description)
);

INSERT INTO product (name, description, price, category, stock_quantity, is_active) VALUES
('Laptop Pro 16"', 'High-performance laptop with M2 chip, 16GB RAM, 512GB SSD', 2499.99, 'Electronics', 25, true),
('Wireless Headphones', 'Premium noise-canceling wireless headphones with 30-hour battery', 299.99, 'Electronics', 50, true),
('Standing Desk', 'Adjustable height standing desk with memory presets', 599.99, 'Furniture', 15, true),
('Coffee Maker', 'Programmable drip coffee maker with thermal carafe', 89.99, 'Appliances', 30, true),
('Running Shoes', 'Lightweight running shoes with advanced cushioning technology', 129.99, 'Sports', 100, true),
('Smartphone Case', 'Protective case with wireless charging support', 39.99, 'Electronics', 200, true),
('Office Chair', 'Ergonomic office chair with lumbar support and armrests', 399.99, 'Furniture', 20, true),
('Bluetooth Speaker', 'Portable waterproof speaker with 12-hour battery life', 79.99, 'Electronics', 75, true),
('Yoga Mat', 'Non-slip eco-friendly yoga mat with carrying strap', 49.99, 'Sports', 60, true),
('Smart Watch', 'Fitness tracking smartwatch with heart rate monitor', 249.99, 'Electronics', 40, true);
api_test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Product API Test Client</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .json-response { 
            background: #f8f9fa; 
            border-left: 4px solid #007bff; 
            font-family: monospace; 
            white-space: pre-wrap; 
        }
    </style>
</head>
<body>
    <div class="container mt-4">
        <h2>Product API Test Client</h2>

        <div class="row">
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h5>API Operations</h5>
                    </div>
                    <div class="card-body">
                        <!-- List Products -->
                        <div class="mb-3">
                            <h6>List Products</h6>
                            <div class="row g-2 mb-2">
                                <div class="col-4">
                                    <input type="number" class="form-control form-control-sm" id="listPage" placeholder="Page" value="1">
                                </div>
                                <div class="col-4">
                                    <input type="number" class="form-control form-control-sm" id="listLimit" placeholder="Limit" value="5">
                                </div>
                                <div class="col-4">
                                    <input type="text" class="form-control form-control-sm" id="listCategory" placeholder="Category">
                                </div>
                            </div>
                            <input type="text" class="form-control form-control-sm mb-2" id="listSearch" placeholder="Search products...">
                            <button class="btn btn-primary btn-sm" onclick="listProducts()">GET /api/products</button>
                        </div>

                        <hr>

                        <!-- Get Product -->
                        <div class="mb-3">
                            <h6>Get Product by ID</h6>
                            <div class="input-group input-group-sm mb-2">
                                <input type="number" class="form-control" id="getProductId" placeholder="Product ID">
                                <button class="btn btn-info" onclick="getProduct()">GET /api/products/{id}</button>
                            </div>
                        </div>

                        <hr>

                        <!-- Create Product -->
                        <div class="mb-3">
                            <h6>Create Product</h6>
                            <input type="text" class="form-control form-control-sm mb-1" id="createName" placeholder="Product Name">
                            <input type="number" class="form-control form-control-sm mb-1" id="createPrice" placeholder="Price" step="0.01">
                            <input type="text" class="form-control form-control-sm mb-1" id="createCategory" placeholder="Category">
                            <textarea class="form-control form-control-sm mb-1" id="createDescription" placeholder="Description" rows="2"></textarea>
                            <input type="number" class="form-control form-control-sm mb-1" id="createStock" placeholder="Stock Quantity">
                            <div class="form-check form-check-inline mb-2">
                                <input class="form-check-input" type="checkbox" id="createActive" checked>
                                <label class="form-check-label" for="createActive">Active</label>
                            </div>
                            <button class="btn btn-success btn-sm" onclick="createProduct()">POST /api/products</button>
                        </div>

                        <hr>

                        <!-- Update Product -->
                        <div class="mb-3">
                            <h6>Update Product</h6>
                            <input type="number" class="form-control form-control-sm mb-1" id="updateId" placeholder="Product ID">
                            <input type="text" class="form-control form-control-sm mb-1" id="updateName" placeholder="New Name (optional)">
                            <input type="number" class="form-control form-control-sm mb-1" id="updatePrice" placeholder="New Price (optional)" step="0.01">
                            <button class="btn btn-warning btn-sm" onclick="updateProduct()">PUT /api/products/{id}</button>
                        </div>

                        <hr>

                        <!-- Delete Product -->
                        <div class="mb-3">
                            <h6>Delete Product</h6>
                            <div class="input-group input-group-sm">
                                <input type="number" class="form-control" id="deleteId" placeholder="Product ID">
                                <button class="btn btn-danger" onclick="deleteProduct()">DELETE /api/products/{id}</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="col-md-6">
                <div class="card">
                    <div class="card-header d-flex justify-content-between align-items-center">
                        <h5>API Response</h5>
                        <button class="btn btn-sm btn-outline-secondary" onclick="clearResponse()">Clear</button>
                    </div>
                    <div class="card-body">
                        <div id="responseContainer" class="json-response p-3">Make an API call to see the response...</div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        const API_BASE = '/t/example/product_api';

        function showResponse(data, status = 200) {
            const container = document.getElementById('responseContainer');
            const statusClass = status >= 200 && status < 300 ? 'border-success' : 'border-danger';
            container.className = `json-response p-3 border ${statusClass}`;
            container.textContent = JSON.stringify(data, null, 2);
        }

        function showError(message) {
            showResponse({ error: message }, 500);
        }

        async function listProducts() {
            try {
                const params = new URLSearchParams();
                const page = document.getElementById('listPage').value;
                const limit = document.getElementById('listLimit').value;
                const category = document.getElementById('listCategory').value;
                const search = document.getElementById('listSearch').value;

                if (page) params.append('page', page);
                if (limit) params.append('limit', limit);
                if (category) params.append('category', category);
                if (search) params.append('search', search);

                const response = await fetch(`${API_BASE}/list?${params}`);
                const data = await response.json();
                showResponse(data, response.status);
            } catch (error) {
                showError(error.message);
            }
        }

        async function getProduct() {
            try {
                const id = document.getElementById('getProductId').value;
                if (!id) {
                    showError('Product ID is required');
                    return;
                }

                const response = await fetch(`${API_BASE}/get?id=${id}`);
                const data = await response.json();
                showResponse(data, response.status);
            } catch (error) {
                showError(error.message);
            }
        }

        async function createProduct() {
            try {
                const formData = new FormData();
                formData.append('name', document.getElementById('createName').value);
                formData.append('price', document.getElementById('createPrice').value);
                formData.append('category', document.getElementById('createCategory').value);
                formData.append('description', document.getElementById('createDescription').value);
                formData.append('stock_quantity', document.getElementById('createStock').value || '0');
                formData.append('is_active', document.getElementById('createActive').checked ? 'true' : 'false');

                const response = await fetch(`${API_BASE}/create`, {
                    method: 'POST',
                    body: formData
                });

                const data = await response.json();
                showResponse(data, response.status);

                // Clear form on success
                if (data.success) {
                    document.getElementById('createName').value = '';
                    document.getElementById('createPrice').value = '';
                    document.getElementById('createCategory').value = '';
                    document.getElementById('createDescription').value = '';
                    document.getElementById('createStock').value = '';
                }
            } catch (error) {
                showError(error.message);
            }
        }

        async function updateProduct() {
            try {
                const id = document.getElementById('updateId').value;
                if (!id) {
                    showError('Product ID is required');
                    return;
                }

                const formData = new FormData();
                formData.append('id', id);

                const name = document.getElementById('updateName').value;
                const price = document.getElementById('updatePrice').value;

                if (name) formData.append('name', name);
                if (price) formData.append('price', price);

                const response = await fetch(`${API_BASE}/update`, {
                    method: 'POST',
                    body: formData
                });

                const data = await response.json();
                showResponse(data, response.status);
            } catch (error) {
                showError(error.message);
            }
        }

        async function deleteProduct() {
            try {
                const id = document.getElementById('deleteId').value;
                if (!id) {
                    showError('Product ID is required');
                    return;
                }

                if (!confirm('Are you sure you want to delete this product?')) {
                    return;
                }

                const response = await fetch(`${API_BASE}/delete`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    },
                    body: `id=${id}`
                });

                const data = await response.json();
                showResponse(data, response.status);
            } catch (error) {
                showError(error.message);
            }
        }

        function clearResponse() {
            const container = document.getElementById('responseContainer');
            container.className = 'json-response p-3';
            container.textContent = 'Make an API call to see the response...';
        }
    </script>
</body>
</html>

REST API Features

  • CRUD Operations: Complete Create, Read, Update, Delete functionality
  • Pagination: Efficient pagination with metadata
  • Search & Filtering: Query parameters for searching and filtering
  • Validation: Comprehensive input validation and error handling
  • HTTP Status Codes: Proper status codes (200, 201, 400, 404, 500)

API with Authentication

secure_api.py
from utils import render, Log
from models import User, ApiKey
import json
import hashlib
import time

class Secure_api(object):
    def authenticate(self, ctx):
        """POST /api/auth - Authenticate and get API key"""
        try:
            request = ctx.getRequest()
            username = request.getParameter("username")
            password = request.getParameter("password")

            if not username or not password:
                return self._error_response(ctx, "Username and password required", 401)

            # Find user (in production, use proper password hashing)
            user = User.findFirst("username = ? AND password = ?", username, password)

            if not user:
                return self._error_response(ctx, "Invalid credentials", 401)

            # Generate API key
            api_key = self._generate_api_key(user.get("id"))

            # Store API key
            api_key_record = ApiKey()
            api_key_record.set("user_id", user.get("id"))
            api_key_record.set("api_key", api_key)
            api_key_record.set("expires_at", int(time.time()) + (7 * 24 * 60 * 60))  # 7 days
            api_key_record.save()

            response = {
                "success": True,
                "data": {
                    "api_key": api_key,
                    "user_id": user.get("id"),
                    "username": user.get("username"),
                    "expires_in": 7 * 24 * 60 * 60  # seconds
                }
            }

            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in authentication: %s" % str(e))
            self._error_response(ctx, "Authentication failed", 500)

    def profile(self, ctx):
        """GET /api/profile - Get user profile (requires authentication)"""
        user = self._authenticate_request(ctx)
        if not user:
            return  # Error already sent

        try:
            response = {
                "success": True,
                "data": {
                    "id": user.get("id"),
                    "username": user.get("username"),
                    "email": user.get("email"),
                    "full_name": user.get("full_name"),
                    "created_at": str(user.get("created_at"))
                }
            }

            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in profile API: %s" % str(e))
            self._error_response(ctx, "Internal server error", 500)

    def update_profile(self, ctx):
        """PUT /api/profile - Update user profile"""
        user = self._authenticate_request(ctx)
        if not user:
            return

        try:
            request = ctx.getRequest()

            # Update allowed fields
            email = request.getParameter("email")
            full_name = request.getParameter("full_name")

            if email:
                if "@" not in email:
                    return self._error_response(ctx, "Invalid email format", 400)
                user.set("email", email)

            if full_name:
                user.set("full_name", full_name)

            user.save()

            response = {
                "success": True,
                "message": "Profile updated successfully",
                "data": {
                    "id": user.get("id"),
                    "username": user.get("username"),
                    "email": user.get("email"),
                    "full_name": user.get("full_name"),
                    "updated_at": str(user.get("updated_at"))
                }
            }

            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in profile update: %s" % str(e))
            self._error_response(ctx, "Internal server error", 500)

    def logout(self, ctx):
        """POST /api/logout - Invalidate API key"""
        try:
            request = ctx.getRequest()
            api_key = request.getHeader("X-API-Key") or request.getParameter("api_key")

            if api_key:
                # Remove API key from database
                ApiKey.delete("api_key = ?", api_key)

            response = {
                "success": True,
                "message": "Logged out successfully"
            }

            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in logout: %s" % str(e))
            self._error_response(ctx, "Internal server error", 500)

    def _authenticate_request(self, ctx):
        """Authenticate API request using API key"""
        try:
            request = ctx.getRequest()
            api_key = request.getHeader("X-API-Key") or request.getParameter("api_key")

            if not api_key:
                self._error_response(ctx, "API key required", 401)
                return None

            # Find valid API key
            api_key_record = ApiKey.findFirst("api_key = ? AND expires_at > ?", api_key, int(time.time()))

            if not api_key_record:
                self._error_response(ctx, "Invalid or expired API key", 401)
                return None

            # Get user
            user = User.findById(api_key_record.get("user_id"))

            if not user:
                self._error_response(ctx, "User not found", 401)
                return None

            return user

        except Exception as e:
            Log.error(ctx, "Authentication error: %s" % str(e))
            self._error_response(ctx, "Authentication failed", 401)
            return None

    def _generate_api_key(self, user_id):
        """Generate unique API key"""
        data = "%s:%s:%s" % (user_id, int(time.time()), "secret_salt")
        return hashlib.sha256(data.encode()).hexdigest()

    def _error_response(self, ctx, message, status_code=400):
        """Send error response"""
        response = {
            "success": False,
            "error": {
                "message": message,
                "code": status_code
            }
        }
        ctx.getResponse().setStatus(status_code)
        ctx.go_to = render.as_json(ctx, response)
CREATE TABLE user (
    id int NOT NULL AUTO_INCREMENT,
    username varchar(50) NOT NULL UNIQUE,
    password varchar(255) NOT NULL,
    email varchar(150),
    full_name varchar(100),
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_username (username)
);

CREATE TABLE api_key (
    id int NOT NULL AUTO_INCREMENT,
    user_id int NOT NULL,
    api_key varchar(64) NOT NULL UNIQUE,
    expires_at int NOT NULL,
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_api_key (api_key),
    KEY idx_user_id (user_id),
    KEY idx_expires (expires_at),
    FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
);

INSERT INTO user (username, password, email, full_name) VALUES
('admin', 'admin123', '[email protected]', 'System Administrator'),
('john_doe', 'password123', '[email protected]', 'John Doe'),
('jane_smith', 'secret456', '[email protected]', 'Jane Smith');
auth_test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Authenticated API Test</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .api-key { font-family: monospace; font-size: 0.85em; }
    </style>
</head>
<body>
    <div class="container mt-4">
        <h2>Authenticated API Test</h2>

        <!-- API Key Display -->
        <div class="alert alert-info" id="apiKeyAlert" style="display: none;">
            <strong>API Key:</strong> <span id="apiKeyDisplay" class="api-key"></span>
            <button class="btn btn-sm btn-outline-secondary ms-2" onclick="clearApiKey()">Clear</button>
        </div>

        <div class="row">
            <div class="col-md-6">
                <!-- Login -->
                <div class="card mb-3">
                    <div class="card-header"><h5>Authentication</h5></div>
                    <div class="card-body">
                        <div class="mb-2">
                            <input type="text" class="form-control form-control-sm" id="username" placeholder="Username" value="admin">
                        </div>
                        <div class="mb-2">
                            <input type="password" class="form-control form-control-sm" id="password" placeholder="Password" value="admin123">
                        </div>
                        <button class="btn btn-primary btn-sm" onclick="login()">Login</button>
                        <button class="btn btn-secondary btn-sm" onclick="logout()">Logout</button>
                    </div>
                </div>

                <!-- Profile Operations -->
                <div class="card">
                    <div class="card-header"><h5>Profile Operations</h5></div>
                    <div class="card-body">
                        <button class="btn btn-info btn-sm mb-2" onclick="getProfile()">Get Profile</button><br>

                        <div class="mb-2">
                            <input type="email" class="form-control form-control-sm" id="updateEmail" placeholder="New Email">
                        </div>
                        <div class="mb-2">
                            <input type="text" class="form-control form-control-sm" id="updateFullName" placeholder="New Full Name">
                        </div>
                        <button class="btn btn-warning btn-sm" onclick="updateProfile()">Update Profile</button>
                    </div>
                </div>
            </div>

            <div class="col-md-6">
                <div class="card">
                    <div class="card-header"><h5>Response</h5></div>
                    <div class="card-body">
                        <pre id="response" class="bg-light p-3">Login to start testing...</pre>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        let apiKey = localStorage.getItem('apiKey');
        if (apiKey) {
            showApiKey(apiKey);
        }

        function showApiKey(key) {
            apiKey = key;
            localStorage.setItem('apiKey', key);
            document.getElementById('apiKeyDisplay').textContent = key;
            document.getElementById('apiKeyAlert').style.display = 'block';
        }

        function clearApiKey() {
            apiKey = null;
            localStorage.removeItem('apiKey');
            document.getElementById('apiKeyAlert').style.display = 'none';
            showResponse({message: 'API key cleared'});
        }

        function showResponse(data, status = 200) {
            document.getElementById('response').textContent = JSON.stringify(data, null, 2);
        }

        async function login() {
            try {
                const username = document.getElementById('username').value;
                const password = document.getElementById('password').value;

                const formData = new FormData();
                formData.append('username', username);
                formData.append('password', password);

                const response = await fetch('/t/example/secure_api/authenticate', {
                    method: 'POST',
                    body: formData
                });

                const data = await response.json();
                showResponse(data, response.status);

                if (data.success && data.data.api_key) {
                    showApiKey(data.data.api_key);
                }
            } catch (error) {
                showResponse({error: error.message});
            }
        }

        async function getProfile() {
            if (!apiKey) {
                showResponse({error: 'Please login first'});
                return;
            }

            try {
                const response = await fetch('/t/example/secure_api/profile', {
                    headers: {
                        'X-API-Key': apiKey
                    }
                });

                const data = await response.json();
                showResponse(data, response.status);
            } catch (error) {
                showResponse({error: error.message});
            }
        }

        async function updateProfile() {
            if (!apiKey) {
                showResponse({error: 'Please login first'});
                return;
            }

            try {
                const formData = new FormData();
                const email = document.getElementById('updateEmail').value;
                const fullName = document.getElementById('updateFullName').value;

                if (email) formData.append('email', email);
                if (fullName) formData.append('full_name', fullName);

                const response = await fetch('/t/example/secure_api/update_profile', {
                    method: 'POST',
                    headers: {
                        'X-API-Key': apiKey
                    },
                    body: formData
                });

                const data = await response.json();
                showResponse(data, response.status);
            } catch (error) {
                showResponse({error: error.message});
            }
        }

        async function logout() {
            if (!apiKey) {
                showResponse({message: 'Not logged in'});
                return;
            }

            try {
                const response = await fetch('/t/example/secure_api/logout', {
                    method: 'POST',
                    headers: {
                        'X-API-Key': apiKey
                    }
                });

                const data = await response.json();
                showResponse(data, response.status);

                if (data.success) {
                    clearApiKey();
                }
            } catch (error) {
                showResponse({error: error.message});
            }
        }
    </script>
</body>
</html>

Authentication Features

  • API Key Authentication: Secure token-based authentication
  • Session Management: API key generation and expiration
  • Protected Endpoints: Authentication required for sensitive operations
  • User Management: Profile updates and logout functionality

API with CORS and Error Handling

cors_api.py
from utils import render, Log
from models import Article
import json

class Cors_api(object):
    def __init__(self):
        # CORS configuration
        self.ALLOWED_ORIGINS = [
            "http://localhost:3000",
            "http://localhost:8080",
            "https://myapp.com"
        ]
        self.ALLOWED_METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
        self.ALLOWED_HEADERS = ["Content-Type", "Authorization", "X-API-Key"]

    def options(self, ctx):
        """Handle CORS preflight requests"""
        self._set_cors_headers(ctx)
        ctx.getResponse().setStatus(200)
        ctx.go_to = render.as_json(ctx, {"status": "ok"})

    def list(self, ctx):
        """GET /api/articles - List articles with CORS support"""
        self._set_cors_headers(ctx)

        try:
            request = ctx.getRequest()

            # Pagination
            page = int(request.getParameter("page") or "1")
            limit = min(int(request.getParameter("limit") or "10"), 50)  # Max 50 items
            offset = (page - 1) * limit

            # Filters
            status = request.getParameter("status") or "published"
            category = request.getParameter("category")

            # Build query
            query_conditions = ["status = ?"]
            query_params = [status]

            if category:
                query_conditions.append("category = ?")
                query_params.append(category)

            query = " AND ".join(query_conditions)

            # Get articles
            articles = Article.where(query, *query_params).offset(offset).limit(limit).orderBy("created_at DESC")
            total_count = Article.count(query, *query_params)

            # Format response
            article_list = []
            for article in articles:
                article_list.append({
                    "id": article.get("id"),
                    "title": article.get("title"),
                    "slug": article.get("slug"),
                    "excerpt": article.get("excerpt"),
                    "category": article.get("category"),
                    "author": article.get("author"),
                    "status": article.get("status"),
                    "published_at": str(article.get("published_at")),
                    "created_at": str(article.get("created_at"))
                })

            response = {
                "success": True,
                "data": article_list,
                "meta": {
                    "page": page,
                    "limit": limit,
                    "total": total_count,
                    "pages": (total_count + limit - 1) // limit
                }
            }

            ctx.go_to = render.as_json(ctx, response)

        except ValueError as e:
            self._validation_error(ctx, "Invalid parameter format: %s" % str(e))
        except Exception as e:
            Log.error(ctx, "Error in articles list: %s" % str(e))
            self._server_error(ctx, "Failed to fetch articles")

    def get(self, ctx):
        """GET /api/articles/{id} - Get single article"""
        self._set_cors_headers(ctx)

        try:
            request = ctx.getRequest()
            article_id = request.getParameter("id")

            if not article_id:
                return self._validation_error(ctx, "Article ID is required")

            article = Article.findById(article_id)

            if not article:
                return self._not_found_error(ctx, "Article not found")

            response = {
                "success": True,
                "data": {
                    "id": article.get("id"),
                    "title": article.get("title"),
                    "slug": article.get("slug"),
                    "content": article.get("content"),
                    "excerpt": article.get("excerpt"),
                    "category": article.get("category"),
                    "author": article.get("author"),
                    "status": article.get("status"),
                    "published_at": str(article.get("published_at")),
                    "created_at": str(article.get("created_at")),
                    "updated_at": str(article.get("updated_at"))
                }
            }

            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error in article get: %s" % str(e))
            self._server_error(ctx, "Failed to fetch article")

    def create(self, ctx):
        """POST /api/articles - Create new article"""
        self._set_cors_headers(ctx)

        try:
            request = ctx.getRequest()

            # Validate required fields
            title = request.getParameter("title")
            content = request.getParameter("content")
            author = request.getParameter("author")

            validation_errors = []

            if not title or len(title.strip()) < 3:
                validation_errors.append("Title must be at least 3 characters long")

            if not content or len(content.strip()) < 10:
                validation_errors.append("Content must be at least 10 characters long")

            if not author or len(author.strip()) < 2:
                validation_errors.append("Author must be at least 2 characters long")

            if validation_errors:
                return self._validation_error(ctx, validation_errors)

            # Generate slug from title
            slug = self._generate_slug(title)

            # Check for duplicate slug
            existing = Article.count("slug = ?", slug)
            if existing > 0:
                slug = "%s-%s" % (slug, int(time.time()))

            # Create article
            article = Article()
            article.set("title", title.strip())
            article.set("slug", slug)
            article.set("content", content.strip())
            article.set("excerpt", request.getParameter("excerpt") or content.strip()[:200])
            article.set("category", request.getParameter("category") or "General")
            article.set("author", author.strip())
            article.set("status", request.getParameter("status") or "draft")
            article.save()

            response = {
                "success": True,
                "message": "Article created successfully",
                "data": {
                    "id": article.get("id"),
                    "title": article.get("title"),
                    "slug": article.get("slug"),
                    "status": article.get("status"),
                    "created_at": str(article.get("created_at"))
                }
            }

            ctx.getResponse().setStatus(201)
            ctx.go_to = render.as_json(ctx, response)

        except Exception as e:
            Log.error(ctx, "Error creating article: %s" % str(e))
            self._server_error(ctx, "Failed to create article")

    def _set_cors_headers(self, ctx):
        """Set CORS headers for cross-origin requests"""
        request = ctx.getRequest()
        response = ctx.getResponse()

        origin = request.getHeader("Origin")

        # Check if origin is allowed
        if origin in self.ALLOWED_ORIGINS or "*" in self.ALLOWED_ORIGINS:
            response.setHeader("Access-Control-Allow-Origin", origin)

        response.setHeader("Access-Control-Allow-Methods", ", ".join(self.ALLOWED_METHODS))
        response.setHeader("Access-Control-Allow-Headers", ", ".join(self.ALLOWED_HEADERS))
        response.setHeader("Access-Control-Max-Age", "3600")
        response.setHeader("Access-Control-Allow-Credentials", "true")

    def _generate_slug(self, title):
        """Generate URL-friendly slug from title"""
        import re
        slug = title.lower()
        slug = re.sub(r'[^a-z0-9\s-]', '', slug)
        slug = re.sub(r'[\s-]+', '-', slug)
        return slug.strip('-')

    def _validation_error(self, ctx, errors):
        """Return validation error response"""
        if isinstance(errors, str):
            errors = [errors]

        response = {
            "success": False,
            "error": {
                "type": "validation_error",
                "message": "Validation failed",
                "details": errors
            }
        }
        ctx.getResponse().setStatus(400)
        ctx.go_to = render.as_json(ctx, response)

    def _not_found_error(self, ctx, message="Resource not found"):
        """Return 404 error response"""
        response = {
            "success": False,
            "error": {
                "type": "not_found",
                "message": message
            }
        }
        ctx.getResponse().setStatus(404)
        ctx.go_to = render.as_json(ctx, response)

    def _server_error(self, ctx, message="Internal server error"):
        """Return 500 error response"""
        response = {
            "success": False,
            "error": {
                "type": "server_error",
                "message": message
            }
        }
        ctx.getResponse().setStatus(500)
        ctx.go_to = render.as_json(ctx, response)
CREATE TABLE article (
    id int NOT NULL AUTO_INCREMENT,
    title varchar(200) NOT NULL,
    slug varchar(250) NOT NULL UNIQUE,
    content longtext NOT NULL,
    excerpt text,
    category varchar(50) DEFAULT 'General',
    author varchar(100) NOT NULL,
    status enum('draft','published','archived') DEFAULT 'draft',
    published_at timestamp NULL,
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_slug (slug),
    KEY idx_status (status),
    KEY idx_category (category),
    KEY idx_author (author)
);

INSERT INTO article (title, slug, content, excerpt, category, author, status, published_at) VALUES
('Getting Started with SØAD', 'getting-started-with-soad', 'This comprehensive guide will walk you through the basics of SØAD framework...', 'Learn the fundamentals of SØAD framework development', 'Tutorial', 'John Developer', 'published', NOW()),
('Advanced Database Queries', 'advanced-database-queries', 'Explore advanced ActiveJDBC patterns and database optimization techniques...', 'Master database operations in SØAD', 'Advanced', 'Jane Expert', 'published', NOW()),
('Building REST APIs', 'building-rest-apis', 'Complete guide to creating robust REST APIs using SØAD framework...', 'Create professional APIs with SØAD', 'API', 'Mike Architect', 'published', NOW()),
('Frontend Integration', 'frontend-integration', 'Learn how to integrate modern frontend frameworks with SØAD backend...', 'Connect your frontend with SØAD', 'Frontend', 'Sarah Designer', 'draft', NULL),
('Deployment Strategies', 'deployment-strategies', 'Best practices for deploying SØAD applications in production...', 'Deploy SØAD apps like a pro', 'DevOps', 'Tom Ops', 'published', NOW());

Advanced Features

  • CORS Support: Cross-origin resource sharing for web applications
  • Error Classification: Structured error responses with types and details
  • Input Validation: Comprehensive validation with detailed error messages
  • Slug Generation: SEO-friendly URL generation from titles
  • Status Codes: Proper HTTP status codes for different scenarios

Do you know?

  • CORS headers enable your API to be consumed by web applications from different domains
  • Structured errors make it easier for frontend developers to handle different error scenarios
  • Slug generation creates SEO-friendly URLs for content-based applications
  • Pagination metadata helps frontend applications build proper navigation controls