Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust Edge Gateway

Rust Edge Gateway is a high-performance API gateway that lets you write request handlers in Rust. Your handlers are compiled to native binaries and run as isolated worker processes, providing:

  • πŸš€ Native Performance - Handlers compile to optimized native code
  • πŸ”’ Isolation - Each handler runs in its own process
  • πŸ”„ Hot Reload - Update handlers without restarting the gateway
  • πŸ› οΈ Simple SDK - Easy-to-use Request/Response API
  • πŸ“¦ Service Integration - Connect to databases, Redis, and more

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Client    │────▢│  Edge Gateway    │────▢│  Your Handler   β”‚
β”‚  (Browser,  β”‚     β”‚  (Routes &       β”‚     β”‚  (Compiled      β”‚
β”‚   API, etc) │◀────│   Manages)       │◀────│   Rust Binary)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
                            β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   Services    β”‚
                    β”‚  (DB, Redis,  β”‚
                    β”‚   MinIO, etc) β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. Gateway receives request - The gateway matches the incoming request to an endpoint
  2. Handler is invoked - The compiled handler binary receives the request via IPC
  3. Handler processes - Your code runs, optionally using injected services
  4. Response returned - The handler sends the response back through the gateway

Getting Started

The fastest way to get started is to:

  1. Access the Admin UI at /admin/
  2. Create a new endpoint
  3. Write your handler code
  4. Compile and test

See the Quick Start guide for detailed instructions.

SDK Overview

Your handler code uses the rust-edge-gateway-sdk crate:

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    Response::ok(json!({
        "message": "Hello, World!",
        "path": req.path,
        "method": req.method,
    }))
}

handler_loop!(handle);
}

The SDK provides:

  • Request - Access HTTP method, path, headers, body, query params
  • Response - Build HTTP responses with JSON, text, or custom content
  • HandlerError - Structured error handling with HTTP status codes
  • Services - Database, Redis, and other service integrations

Architecture

Rust Edge Gateway uses a worker process model:

  • Main Gateway - Axum-based HTTP server handling routing
  • Worker Processes - Your compiled handlers as standalone binaries
  • IPC Protocol - Length-prefixed JSON over stdin/stdout
  • Service Connectors - Pooled connections to backends (DB, Redis, etc.)

This architecture provides:

  • Security - Handlers can't directly access the gateway's memory
  • Stability - A crashed handler doesn't bring down the gateway
  • Scalability - Multiple worker instances can handle concurrent requests

Quick Start

This guide will help you create your first Rust Edge Gateway endpoint in under 5 minutes.

Prerequisites

  • Rust Edge Gateway running (either locally via Docker or deployed)
  • Access to the Admin UI

Step 1: Access the Admin UI

Navigate to your gateway's admin interface:

  • Local Development: http://localhost:9081/admin/
  • Production: https://rust-edge-gateway.yourdomain.com/admin/

Step 2: Create an Endpoint

  1. Click "Create Endpoint" or the + button
  2. Fill in the endpoint details:
FieldExample ValueDescription
Namehello-worldUnique identifier for your endpoint
Path/helloThe URL path to match
MethodGETHTTP method (GET, POST, PUT, DELETE, etc.)
Domain*Domain to match (or * for all)
  1. Click Save

Step 3: Write Handler Code

In the code editor, replace the default code with:

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    Response::ok(json!({
        "message": "Hello from Rust Edge Gateway!",
        "path": req.path,
        "method": req.method,
        "timestamp": chrono::Utc::now().to_rfc3339(),
    }))
}

handler_loop!(handle);
}

Step 4: Compile

Click the "Compile" button. The gateway will:

  1. Generate a Cargo project with your code
  2. Compile it to a native binary
  3. Report success or any compilation errors

You should see a success message like:

βœ“ Compiled successfully in 2.3s

Step 5: Start the Endpoint

Click "Start" to activate the endpoint. The status should change to Running.

Step 6: Test Your Endpoint

Make a request to your endpoint:

curl http://localhost:9080/hello

You should receive:

{
  "message": "Hello from Rust Edge Gateway!",
  "path": "/hello",
  "method": "GET",
  "timestamp": "2024-01-15T10:30:00.000Z"
}

What's Next?

Troubleshooting

Compilation Errors

Check the error message for:

  • Missing dependencies (add to your handler's use statements)
  • Syntax errors (Rust compiler messages are helpful!)
  • Type mismatches

Endpoint Not Responding

  1. Check the endpoint is in Running status
  2. Verify the path matches exactly (paths are case-sensitive)
  3. Check the method matches your request
  4. View endpoint logs in the admin UI

Handler Crashes

View the logs to see panic messages or error output. Common causes:

  • Unwrapping None or Err values
  • Stack overflow from deep recursion
  • Accessing invalid JSON fields

Your First Handler

This guide explains the structure of a handler and how to work with requests and responses.

Handler Structure

Every handler follows the same pattern:

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    // Your logic here
    Response::ok(json!({"status": "success"}))
}

handler_loop!(handle);
}

The Prelude

The prelude module imports everything you typically need:

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

// This imports:
// - Request, Response types
// - serde::{Deserialize, Serialize}
// - serde_json::{json, Value as JsonValue}
// - read_request, send_response IPC functions
// - HandlerError for error handling
}

The Handler Function

Your handler function receives a Request and returns a Response:

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    // Access request data
    let method = &req.method;  // "GET", "POST", etc.
    let path = &req.path;      // "/users/123"
    
    // Return a response
    Response::ok(json!({"received": path}))
}
}

The Handler Loop Macro

The handler_loop! macro sets up the main function and IPC loop:

handler_loop!(handle);

// This expands to:
fn main() {
    loop {
        match read_request() {
            Ok(req) => {
                let response = handle(req);
                send_response(response).unwrap();
            }
            Err(_) => break,
        }
    }
}

Working with Requests

Accessing the Body

For POST/PUT requests, parse the JSON body:

#![allow(unused)]
fn main() {
#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

fn handle(req: Request) -> Response {
    // Parse JSON body
    let user: CreateUser = match req.json() {
        Ok(u) => u,
        Err(e) => return Response::bad_request(format!("Invalid JSON: {}", e)),
    };
    
    Response::created(json!({
        "id": "new-user-id",
        "name": user.name,
        "email": user.email,
    }))
}
}

Path Parameters

Extract dynamic path segments (e.g., /users/{id}):

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let user_id = req.path_param("id")
        .ok_or_else(|| "Missing user ID")?;
    
    Response::ok(json!({"user_id": user_id}))
}
}

Query Parameters

Access query string values (e.g., ?page=1&limit=10):

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let page = req.query_param("page")
        .map(|s| s.parse::<u32>().unwrap_or(1))
        .unwrap_or(1);
    
    let limit = req.query_param("limit")
        .map(|s| s.parse::<u32>().unwrap_or(10))
        .unwrap_or(10);
    
    Response::ok(json!({
        "page": page,
        "limit": limit,
    }))
}
}

Headers

Access HTTP headers (case-insensitive):

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let auth = req.header("Authorization");
    let content_type = req.header("Content-Type");
    
    if auth.is_none() {
        return Response::json(401, json!({"error": "Unauthorized"}));
    }
    
    Response::ok(json!({"authenticated": true}))
}
}

Working with Responses

JSON Responses

The most common response type:

#![allow(unused)]
fn main() {
// 200 OK with JSON
Response::ok(json!({"status": "success"}))

// 201 Created
Response::created(json!({"id": "123"}))

// Custom status with JSON
Response::json(418, json!({"error": "I'm a teapot"}))
}

Error Responses

Built-in error response helpers:

#![allow(unused)]
fn main() {
Response::bad_request("Invalid input")      // 400
Response::not_found()                        // 404
Response::internal_error("Something broke")  // 500
}

Custom Headers

Add headers to any response:

#![allow(unused)]
fn main() {
Response::ok(json!({"data": "value"}))
    .with_header("X-Custom-Header", "custom-value")
    .with_header("Cache-Control", "max-age=3600")
}

Text Responses

For non-JSON responses:

#![allow(unused)]
fn main() {
Response::text(200, "Hello, World!")
Response::text(200, "<html><body>Hello</body></html>")
    .with_header("Content-Type", "text/html")
}

Next Steps

Handler Lifecycle

Understanding how handlers are compiled, started, and managed helps you write more reliable code.

Endpoint States

An endpoint can be in one of these states:

StateDescription
CreatedEndpoint defined but code not yet compiled
CompiledCode compiled successfully, ready to start
RunningWorker process is active and handling requests
StoppedWorker process stopped, can be restarted
ErrorCompilation or runtime error occurred

Compilation

When you click "Compile", the gateway:

  1. Creates a Cargo project in the handlers directory
  2. Writes your code to src/main.rs
  3. Generates Cargo.toml with the SDK dependency
  4. Runs cargo build --release to compile
  5. Stores the binary for execution

Generated Project Structure

handlers/
└── {endpoint-id}/
    β”œβ”€β”€ Cargo.toml
    β”œβ”€β”€ Cargo.lock
    β”œβ”€β”€ src/
    β”‚   └── main.rs    # Your handler code
    └── target/
        └── release/
            └── handler  # Compiled binary

Cargo.toml

The generated Cargo.toml includes the SDK:

[package]
name = "handler"
version = "0.1.0"
edition = "2021"

[dependencies]
rust-edge-gateway-sdk = { path = "../../crates/rust-edge-gateway-sdk" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

You can request additional dependencies by using them in your code - the gateway will detect common crates like chrono, uuid, regex, etc.

Worker Processes

Starting a Handler

When you start an endpoint:

  1. Gateway spawns the compiled binary as a child process
  2. IPC channels are established (stdin/stdout)
  3. Worker enters its request loop
  4. Status changes to "Running"

Request Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Gateway │────▢│  stdin   │────▢│ Worker β”‚
β”‚         β”‚     β”‚ (request)β”‚     β”‚        β”‚
β”‚         │◀────│  stdout  │◀────│        β”‚
β”‚         β”‚     β”‚(response)β”‚     β”‚        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The IPC protocol uses length-prefixed JSON:

  • 4 bytes: message length (big-endian u32)
  • N bytes: JSON payload

Worker Loop

Your handler runs in a loop:

#![allow(unused)]
fn main() {
loop {
    // 1. Read request from stdin
    let request = read_request()?;
    
    // 2. Call your handler function
    let response = handle(request);
    
    // 3. Write response to stdout
    send_response(response)?;
}
}

The loop exits when:

  • stdin is closed (gateway stopped the worker)
  • An IPC error occurs
  • The process is killed

Stopping a Handler

When you stop an endpoint:

  1. Gateway closes the stdin pipe
  2. Worker's read_request() returns an error
  3. Worker exits cleanly
  4. Gateway waits for process exit
  5. Status changes to "Stopped"

Hot Reload

Rust Edge Gateway supports hot reloading:

  1. Edit code in the admin UI
  2. Compile the new version
  3. Restart the endpoint
    • Old worker finishes current request
    • New worker starts with updated code

No gateway restart required!

Error Handling

Compilation Errors

If compilation fails:

  • Error message is captured and displayed
  • Endpoint stays in previous state
  • Previous binary (if any) remains available

Runtime Errors

If your handler panics:

  • Gateway detects the worker exit
  • Error is logged
  • Worker can be restarted
  • Endpoint moves to "Error" state

Graceful Error Handling

Always handle errors in your code:

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    match process_request(&req) {
        Ok(data) => Response::ok(data),
        Err(e) => e.to_response(), // HandlerError -> Response
    }
}

fn process_request(req: &Request) -> Result<JsonValue, HandlerError> {
    let body: MyInput = req.json()
        .map_err(|e| HandlerError::ValidationError(e.to_string()))?;
    
    // ... process ...
    
    Ok(json!({"result": "success"}))
}
}

Handler Macros

The SDK provides several macros for running handler loops with different patterns.

Quick Reference

MacroHandler SignatureUse Case
handler_loop!fn(Request) -> ResponseSimple sync handlers
handler_loop_result!fn(Request) -> Result<Response, HandlerError>Sync with error handling
handler_loop_async!async fn(Request) -> ResponseAsync handlers
handler_loop_async_result!async fn(Request) -> Result<Response, HandlerError>Async with error handling

Sync Handlers

handler_loop!

For simple synchronous handlers that return a Response directly.

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    Response::ok(json!({"path": req.path, "method": req.method}))
}

handler_loop!(handle);
}

handler_loop_result!

For handlers that return Result<Response, HandlerError>. Errors are automatically converted to HTTP responses.

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

#[derive(Deserialize)]
struct CreateItem {
    name: String,
    price: f64,
}

fn handle(req: Request) -> Result<Response, HandlerError> {
    // These all use ? operator - errors become HTTP responses
    let auth = req.require_header("Authorization")?;
    let item: CreateItem = req.json()?;
    let id: i64 = req.require_path_param("id")?;
    
    if item.price < 0.0 {
        return Err(HandlerError::ValidationError("Price cannot be negative".into()));
    }
    
    Ok(Response::created(json!({"id": id, "item": item.name})))
}

handler_loop_result!(handle);
}

Async Handlers

Async handlers require the async feature to be enabled.

Cargo.toml

[dependencies]
rust-edge-gateway-sdk = { git = "https://github.com/user/rust-edge-gateway", features = ["async"] }
tokio = { version = "1", features = ["full"] }

handler_loop_async!

For async handlers that return a Response directly. A Tokio runtime is created automatically.

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

async fn handle(req: Request) -> Response {
    let data = fetch_from_api().await;
    Response::ok(data)
}

handler_loop_async!(handle);
}

handler_loop_async_result!

For async handlers with Result-based error handling. Combines async support with automatic error conversion.

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

async fn handle(req: Request) -> Result<Response, HandlerError> {
    let data: CreateItem = req.json()?;
    
    // Async database call
    let id = database.insert(&data).await
        .map_err(|e| HandlerError::DatabaseError(e.to_string()))?;
    
    // Async file upload
    let url = s3.upload(&data.file).await
        .map_err(|e| HandlerError::StorageError(e.to_string()))?;
    
    Ok(Response::created(json!({
        "id": id,
        "file_url": url
    })))
}

handler_loop_async_result!(handle);
}

Runtime Management

Important: The async macros create a single Tokio runtime that persists across all requests. This is more efficient than creating a runtime per request.

#![allow(unused)]
fn main() {
// DON'T do this - creates runtime per request (inefficient):
fn handle(req: Request) -> Response {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        // async work
    })
}

// DO this - runtime is created once by the macro:
async fn handle(req: Request) -> Response {
    // async work directly
}
handler_loop_async!(handle);
}

Choosing the Right Macro

Your handler needs...Use this macro
Simple sync logichandler_loop!
Sync with ? operatorhandler_loop_result!
Async operations (DB, HTTP, files)handler_loop_async!
Async with ? operatorhandler_loop_async_result!

Example: Complete CRUD Handler

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

async fn handle(req: Request) -> Result<Response, HandlerError> {
    match (req.method.as_str(), req.path.as_str()) {
        ("GET", "/items") => list_items().await,
        ("POST", "/items") => create_item(&req).await,
        ("GET", _) if req.path.starts_with("/items/") => get_item(&req).await,
        ("DELETE", _) if req.path.starts_with("/items/") => delete_item(&req).await,
        _ => Err(HandlerError::MethodNotAllowed("Use GET, POST, or DELETE".into())),
    }
}

async fn list_items() -> Result<Response, HandlerError> {
    let items = db::get_all_items().await
        .map_err(|e| HandlerError::DatabaseError(e.to_string()))?;
    Ok(Response::ok(json!({"items": items})))
}

async fn create_item(req: &Request) -> Result<Response, HandlerError> {
    let item: NewItem = req.json()?;
    let id = db::insert_item(&item).await
        .map_err(|e| HandlerError::DatabaseError(e.to_string()))?;
    Ok(Response::created(json!({"id": id})))
}

handler_loop_async_result!(handle);
}

Request

The Request struct represents an incoming HTTP request.

Definition

#![allow(unused)]
fn main() {
pub struct Request {
    pub method: String,
    pub path: String,
    pub query: HashMap<String, String>,
    pub headers: HashMap<String, String>,
    pub body: Option<String>,
    pub params: HashMap<String, String>,
    pub client_ip: Option<String>,
    pub request_id: String,
}
}

Fields

FieldTypeDescription
methodStringHTTP method: GET, POST, PUT, DELETE, PATCH, etc.
pathStringRequest path, e.g., /users/123
queryHashMap<String, String>Query parameters from the URL
headersHashMap<String, String>HTTP headers
bodyOption<String>Request body (for POST, PUT, PATCH)
paramsHashMap<String, String>Path parameters extracted from the route
client_ipOption<String>Client's IP address
request_idStringUnique identifier for request tracing

Methods Reference

JSON Parsing

json<T>() -> Result<T, HandlerError>

Parse the body as JSON into a typed struct. Returns HandlerError::BadRequest on parse failure.

#![allow(unused)]
fn main() {
#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

fn handle(req: Request) -> Result<Response, HandlerError> {
    let user: CreateUser = req.json()?;  // Uses ? operator naturally
    Ok(Response::ok(json!({"name": user.name})))
}

handler_loop_result!(handle);
}

Query Parameters

query_param(key: &str) -> Option<&String>

Get a query parameter as a string reference.

#![allow(unused)]
fn main() {
// URL: /search?q=rust
let query = req.query_param("q"); // Some(&"rust".to_string())
}

query_param_as<T: FromStr>(key: &str) -> Option<T>

Get a query parameter parsed as a specific type. Returns None if missing or can't be parsed.

#![allow(unused)]
fn main() {
// URL: /items?page=2&limit=10
let page: i64 = req.query_param_as("page").unwrap_or(1);
let limit: usize = req.query_param_as("limit").unwrap_or(20);
let active: bool = req.query_param_as("active").unwrap_or(false);
}

require_query_param<T: FromStr>(key: &str) -> Result<T, HandlerError>

Get a required query parameter. Returns HandlerError::BadRequest if missing or invalid.

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Result<Response, HandlerError> {
    let page: i64 = req.require_query_param("page")?;
    let limit: usize = req.require_query_param("limit")?;
    Ok(Response::ok(json!({"page": page, "limit": limit})))
}
}

Path Parameters

path_param(key: &str) -> Option<&String>

Get a path parameter extracted from the route pattern.

#![allow(unused)]
fn main() {
// Endpoint path: /users/{id}/posts/{post_id}
// Request: /users/123/posts/456

let user_id = req.path_param("id");       // Some(&"123".to_string())
let post_id = req.path_param("post_id");  // Some(&"456".to_string())
}

path_param_as<T: FromStr>(key: &str) -> Option<T>

Get a path parameter parsed as a specific type.

#![allow(unused)]
fn main() {
// Route: /users/{id}
let user_id: i64 = req.path_param_as("id").unwrap_or(0);
let uuid: Uuid = req.path_param_as("id").ok_or(HandlerError::BadRequest("Invalid UUID".into()))?;
}

require_path_param<T: FromStr>(key: &str) -> Result<T, HandlerError>

Get a required path parameter. Returns HandlerError::BadRequest if missing or invalid.

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Result<Response, HandlerError> {
    let user_id: i64 = req.require_path_param("id")?;
    Ok(Response::ok(json!({"user_id": user_id})))
}
}

Headers

header(key: &str) -> Option<&String>

Get a header value (case-insensitive lookup).

#![allow(unused)]
fn main() {
// All of these work:
let auth = req.header("Authorization");
let auth = req.header("authorization");
let auth = req.header("AUTHORIZATION");
}

require_header(key: &str) -> Result<&String, HandlerError>

Get a required header. Returns HandlerError::BadRequest if missing.

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Result<Response, HandlerError> {
    let auth = req.require_header("Authorization")?;
    let api_key = req.require_header("X-API-Key")?;
    Ok(Response::ok(json!({"authenticated": true})))
}
}

Request Inspection

is_method(method: &str) -> bool

Check if the request method matches (case-insensitive).

#![allow(unused)]
fn main() {
if req.is_method("POST") {
    // Handle POST
}
}

content_type() -> Option<&String>

Get the Content-Type header value.

#![allow(unused)]
fn main() {
if let Some(ct) = req.content_type() {
    eprintln!("Content-Type: {}", ct);
}
}

is_json() -> bool

Check if the request has a JSON content type.

#![allow(unused)]
fn main() {
if req.is_json() {
    let data: MyStruct = req.json()?;
}
}

is_multipart() -> bool

Check if the request has a multipart/form-data content type.

#![allow(unused)]
fn main() {
if req.is_multipart() {
    let multipart = req.multipart()?;
}
}

body_bytes() -> Vec<u8>

Get the raw body as bytes.

#![allow(unused)]
fn main() {
let raw_body = req.body_bytes();
}

Multipart Form Data

multipart() -> Result<MultipartData, HandlerError>

Parse multipart/form-data body for file uploads.

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Result<Response, HandlerError> {
    let multipart = req.multipart()?;

    // Get text fields
    let title = multipart.require_field("title")?;
    let description = multipart.field("description").unwrap_or(&"".to_string());

    // Get uploaded file
    let file = multipart.require_file("upload")?;
    eprintln!("Received file: {} ({} bytes)", file.filename, file.data.len());

    Ok(Response::ok(json!({
        "title": title,
        "filename": file.filename,
        "size": file.data.len()
    })))
}
}

MultipartData

Fields

FieldTypeDescription
fieldsHashMap<String, String>Text form fields
filesHashMap<String, MultipartFile>Uploaded files

Methods

MethodReturnsDescription
field(name)Option<&String>Get a text field
require_field(name)Result<&String, HandlerError>Get required text field
file(name)Option<&MultipartFile>Get an uploaded file
require_file(name)Result<&MultipartFile, HandlerError>Get required file

MultipartFile

#![allow(unused)]
fn main() {
pub struct MultipartFile {
    pub filename: String,      // Original filename
    pub content_type: String,  // MIME type
    pub data: Vec<u8>,         // File content
}
}

Complete Example

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

#[derive(Deserialize)]
struct UpdateProfile {
    name: Option<String>,
    bio: Option<String>,
}

fn handle(req: Request) -> Result<Response, HandlerError> {
    // Log request info
    eprintln!("[{}] {} {} from {:?}",
        req.request_id, req.method, req.path, req.client_ip);

    // Check authentication
    let token = req.require_header("Authorization")?;

    // Get path parameter (typed)
    let user_id: i64 = req.require_path_param("id")?;

    // Parse body
    let update: UpdateProfile = req.json()?;

    // Process request...
    Ok(Response::ok(json!({
        "user_id": user_id,
        "updated": true,
    })))
}

handler_loop_result!(handle);
}

Response

The Response struct represents an outgoing HTTP response.

Quick Reference

MethodStatusUse Case
ok(body)200Successful GET/PUT
created(body)201Successful POST
accepted(body)202Async operation started
no_content()204Successful DELETE
bad_request(msg)400Invalid input
unauthorized(msg)401Missing/invalid auth
forbidden(msg)403Not authorized
not_found()404Resource not found
conflict(msg)409Resource conflict
internal_error(msg)500Server error
service_unavailable(msg)503Backend down

Definition

#![allow(unused)]
fn main() {
pub struct Response {
    pub status: u16,
    pub headers: HashMap<String, String>,
    pub body: Option<String>,
}
}

Constructor Methods

new(status: u16)

Create a response with just a status code (no body).

#![allow(unused)]
fn main() {
Response::new(204)  // 204 No Content
Response::new(301).with_header("Location", "/new-path")
}

ok<T: Serialize>(body: T)

Create a 200 OK response with JSON body.

#![allow(unused)]
fn main() {
Response::ok(json!({"message": "Success"}))
Response::ok(my_struct)  // If my_struct implements Serialize
}

json<T: Serialize>(status: u16, body: T)

Create a JSON response with a custom status code.

#![allow(unused)]
fn main() {
Response::json(201, json!({"id": "new-id"}))
Response::json(400, json!({"error": "Invalid input"}))
}

text(status: u16, body: impl Into<String>)

Create a plain text response.

#![allow(unused)]
fn main() {
Response::text(200, "Hello, World!")
Response::text(500, "Internal Server Error")
}

html(status: u16, body: impl Into<String>)

Create an HTML response.

#![allow(unused)]
fn main() {
Response::html(200, "<html><body><h1>Hello!</h1></body></html>")
}

binary(status: u16, data: impl AsRef<[u8]>, content_type: impl Into<String>)

Create a binary response for files, images, etc.

#![allow(unused)]
fn main() {
// Serve an image
Response::binary(200, image_bytes, "image/png")

// Serve a PDF with download prompt
Response::binary(200, pdf_bytes, "application/pdf")
    .with_header("Content-Disposition", "attachment; filename=\"report.pdf\"")

// Inline image
Response::binary(200, jpeg_bytes, "image/jpeg")
    .with_header("Content-Disposition", "inline; filename=\"photo.jpg\"")
}

created<T: Serialize>(body: T)

Create a 201 Created response.

#![allow(unused)]
fn main() {
Response::created(json!({"id": "12345", "name": "New Resource"}))
}

accepted<T: Serialize>(body: T)

Create a 202 Accepted response (for async operations).

#![allow(unused)]
fn main() {
Response::accepted(json!({"job_id": "abc123", "status": "processing"}))
}

no_content()

Create a 204 No Content response.

#![allow(unused)]
fn main() {
Response::no_content()  // Used for DELETE
}

redirect(status: u16, location: impl Into<String>)

Create a redirect response.

#![allow(unused)]
fn main() {
Response::redirect(302, "https://example.com/new-location")
Response::redirect(301, "/permanent-new-path")
}

Error Response Helpers

bad_request(message: impl Into<String>)

400 Bad Request.

#![allow(unused)]
fn main() {
Response::bad_request("Missing required field: email")
}

unauthorized(message: impl Into<String>)

401 Unauthorized.

#![allow(unused)]
fn main() {
Response::unauthorized("Invalid or expired token")
}

forbidden(message: impl Into<String>)

403 Forbidden.

#![allow(unused)]
fn main() {
Response::forbidden("You don't have permission to access this resource")
}

not_found() / not_found_msg(message)

404 Not Found.

#![allow(unused)]
fn main() {
Response::not_found()
Response::not_found_msg("User with ID 123 not found")
}

conflict(message: impl Into<String>)

409 Conflict.

#![allow(unused)]
fn main() {
Response::conflict("A user with this email already exists")
}

internal_error(message: impl Into<String>)

500 Internal Server Error.

#![allow(unused)]
fn main() {
Response::internal_error("Database connection failed")
}

service_unavailable(message: impl Into<String>)

503 Service Unavailable.

#![allow(unused)]
fn main() {
Response::service_unavailable("Database is currently unavailable")
}

Builder Methods

with_header(key, value)

Add a header to the response.

#![allow(unused)]
fn main() {
Response::ok(json!({"data": "value"}))
    .with_header("Cache-Control", "max-age=3600")
    .with_header("X-Custom-Header", "custom-value")
}

with_body(body: impl Into<String>)

Set or replace the response body.

#![allow(unused)]
fn main() {
Response::new(200)
    .with_header("Content-Type", "text/html")
    .with_body("<html><body>Hello!</body></html>")
}

with_cors(origin: impl Into<String>)

Add CORS headers for cross-origin requests.

#![allow(unused)]
fn main() {
Response::ok(data).with_cors("*")
Response::ok(data).with_cors("https://myapp.com")
}

with_cache(max_age_seconds: u32)

Add caching headers.

#![allow(unused)]
fn main() {
Response::ok(data).with_cache(3600)  // Cache for 1 hour
Response::ok(data).with_cache(0)     // No cache
}

Common Patterns

RESTful API

#![allow(unused)]
fn main() {
// GET /items - List items
Response::ok(json!({"items": items}))

// GET /items/{id} - Get single item
Response::ok(item)  // or Response::not_found()

// POST /items - Create item
Response::created(json!({"id": new_id, ...item}))

// PUT /items/{id} - Update item
Response::ok(updated_item)

// DELETE /items/{id} - Delete item
Response::no_content()
}

Error Handling with Result

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Result<Response, HandlerError> {
    let user = find_user(&req)?;  // Returns 404 if not found
    Ok(Response::ok(user))
}

handler_loop_result!(handle);  // Errors auto-convert to Response
}

File Uploads Response

#![allow(unused)]
fn main() {
fn handle_upload(req: Request) -> Result<Response, HandlerError> {
    let multipart = req.multipart()?;
    let file = multipart.require_file("document")?;

    // Process file...
    let saved_id = save_to_storage(&file.data)?;

    Ok(Response::created(json!({
        "id": saved_id,
        "filename": file.filename,
        "size": file.data.len(),
        "content_type": file.content_type
    })))
}
}

Serving Files

#![allow(unused)]
fn main() {
fn serve_image(req: Request) -> Result<Response, HandlerError> {
    let id = req.require_path_param::<String>("id")?;
    let image_data = load_image(&id)?;

    Ok(Response::binary(200, image_data, "image/png")
        .with_cache(86400)  // Cache for 1 day
        .with_header("Content-Disposition", format!("inline; filename=\"{}.png\"", id)))
}
}

Custom Content Types

#![allow(unused)]
fn main() {
// XML
Response::new(200)
    .with_header("Content-Type", "application/xml")
    .with_body("<root><item>Value</item></root>")

// CSV with download
Response::new(200)
    .with_header("Content-Type", "text/csv")
    .with_header("Content-Disposition", "attachment; filename=\"data.csv\"")
    .with_body("id,name\n1,Alice\n2,Bob")
}

CORS Preflight

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    if req.is_method("OPTIONS") {
        return Response::no_content()
            .with_cors("*")
            .with_header("Access-Control-Max-Age", "86400");
    }

    // Handle actual request...
    Response::ok(data).with_cors("*")
}

}

Error Handling

The SDK provides a HandlerError enum for structured error handling with automatic HTTP status code mapping.

Quick Reference

VariantStatusUse Case
BadRequest(msg)400Invalid input, malformed JSON
ValidationError(msg)400Semantic validation failures
Unauthorized(msg)401Missing or invalid auth
Forbidden(msg)403Authenticated but not authorized
NotFound / NotFoundMessage(msg)404Resource not found
MethodNotAllowed(msg)405Wrong HTTP method
Conflict(msg)409Resource conflict (duplicate)
PayloadTooLarge(msg)413Request body too large
Internal(msg) / InternalError(msg)500Server error
DatabaseError(msg)500Database operation failed
StorageError(msg)500Storage operation failed
ServiceUnavailable(msg)503Backend service down

HandlerError Definition

#![allow(unused)]
fn main() {
pub enum HandlerError {
    // 4xx Client Errors
    BadRequest(String),
    ValidationError(String),
    Unauthorized(String),
    Forbidden(String),
    NotFound,
    NotFoundMessage(String),
    MethodNotAllowed(String),
    Conflict(String),
    PayloadTooLarge(String),

    // 5xx Server Errors
    IpcError(String),
    SerializationError(serde_json::Error),
    DatabaseError(String),
    RedisError(String),
    StorageError(String),
    InternalError(String),
    Internal(String),
    ServiceUnavailable(String),
}
}

Key Features

Automatic Response Conversion

HandlerError implements From<HandlerError> for Response, so you can use .into():

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    match process(&req) {
        Ok(data) => Response::ok(data),
        Err(e) => e.into(),  // Automatically converts to Response
    }
}
}

Use with handler_loop_result!

The handler_loop_result! macro automatically converts errors to responses:

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Result<Response, HandlerError> {
    let data: MyInput = req.json()?;  // BadRequest on parse failure
    let id: i64 = req.require_path_param("id")?;  // BadRequest if missing
    let auth = req.require_header("Authorization")?;  // BadRequest if missing

    Ok(Response::ok(json!({"id": id, "data": data})))
}

handler_loop_result!(handle);  // Errors auto-convert to HTTP responses
}

Methods

status_code() -> u16

Get the HTTP status code for this error:

#![allow(unused)]
fn main() {
let err = HandlerError::NotFound;
assert_eq!(err.status_code(), 404);

let err = HandlerError::BadRequest("Invalid input".into());
assert_eq!(err.status_code(), 400);
}

to_response() -> Response

Convert the error to an HTTP Response:

#![allow(unused)]
fn main() {
let err = HandlerError::ValidationError("Invalid email".to_string());
let response = err.to_response();
// Response { status: 400, body: {"error": "Validation error: Invalid email"} }
}

Usage Patterns

Clean Result-Based Handlers

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

#[derive(Deserialize)]
struct CreateUser {
    email: String,
    name: String,
}

fn handle(req: Request) -> Result<Response, HandlerError> {
    // Parse body - returns BadRequest on failure
    let body: CreateUser = req.json()?;

    // Validate
    if body.email.is_empty() {
        return Err(HandlerError::ValidationError("Email is required".into()));
    }

    // Check authentication
    let token = req.require_header("Authorization")?;

    // Get typed path parameter
    let user_id: i64 = req.require_path_param("id")?;

    // Simulate database operation
    let user = find_user(user_id)
        .ok_or(HandlerError::NotFound)?;

    Ok(Response::ok(user))
}

handler_loop_result!(handle);
}

Converting External Errors

Map external library errors to HandlerError:

#![allow(unused)]
fn main() {
fn save_to_database(data: &MyData) -> Result<i64, HandlerError> {
    let conn = get_connection()
        .map_err(|e| HandlerError::DatabaseError(e.to_string()))?;

    let id = conn.insert(data)
        .map_err(|e| HandlerError::DatabaseError(e.to_string()))?;

    Ok(id)
}
}

Custom Error Types

Define domain-specific errors and convert to HandlerError:

#![allow(unused)]
fn main() {
enum AppError {
    UserNotFound(String),
    DuplicateEmail,
    InvalidCredentials,
    RateLimited,
}

impl From<AppError> for HandlerError {
    fn from(e: AppError) -> Self {
        match e {
            AppError::UserNotFound(id) =>
                HandlerError::NotFoundMessage(format!("User {} not found", id)),
            AppError::DuplicateEmail =>
                HandlerError::Conflict("Email already registered".into()),
            AppError::InvalidCredentials =>
                HandlerError::Unauthorized("Invalid email or password".into()),
            AppError::RateLimited =>
                HandlerError::ServiceUnavailable("Rate limit exceeded, try again later".into()),
        }
    }
}

fn handle(req: Request) -> Result<Response, HandlerError> {
    let result = business_logic(&req)
        .map_err(HandlerError::from)?;  // Convert AppError to HandlerError
    Ok(Response::ok(result))
}
}

Async Error Handling

Works the same with async handlers:

#![allow(unused)]
fn main() {
async fn handle(req: Request) -> Result<Response, HandlerError> {
    let data: CreateItem = req.json()?;

    let id = database.insert(&data).await
        .map_err(|e| HandlerError::DatabaseError(e.to_string()))?;

    let uploaded = s3.put_object(&data.file).await
        .map_err(|e| HandlerError::StorageError(e.to_string()))?;

    Ok(Response::created(json!({
        "id": id,
        "file_url": uploaded.url
    })))
}

handler_loop_async_result!(handle);
}

Logging Errors

Always log errors for debugging (logs go to stderr, captured by gateway):

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Result<Response, HandlerError> {
    match process(&req) {
        Ok(data) => Ok(Response::ok(data)),
        Err(e) => {
            eprintln!("[{}] Error: {} ({})",
                req.request_id,
                e,
                e.status_code()
            );
            Err(e)  // Will be converted to Response by handler_loop_result!
        }
    }
}
}

Best Practices

  1. Use handler_loop_result! - Simplifies error handling with automatic conversion
  2. Use specific error variants - BadRequest vs ValidationError vs Unauthorized
  3. Always log errors - Use eprintln! for debugging
  4. Convert early - Map external errors to HandlerError at the boundary
  5. Include context - Error messages should help debugging

Services

Rust Edge Gateway can connect your handlers to backend services like databases, Redis, and object storage.

Overview

Services are configured in the gateway admin UI and made available to handlers. Your handler code uses typed service handles to interact with backends.

Available Service Types

ServiceDescriptionUse Cases
SQLiteEmbedded SQL databaseLocal data, caching, simple apps
PostgreSQLAdvanced relational databaseComplex queries, transactions
MySQLPopular relational databaseWeb applications, compatibility
RedisIn-memory data storeCaching, sessions, pub/sub
MongoDBDocument databaseFlexible schemas, JSON data
MinIOS3-compatible object storageFile uploads, media storage
MemcachedDistributed cachingHigh-speed key-value caching
FTP/SFTPFile transfer protocolsFile uploads, vendor integrations
EmailSMTP email sendingNotifications, alerts, reports

Configuring Services

Via Admin UI

  1. Go to Services in the admin panel
  2. Click Create Service
  3. Select service type and configure connection
  4. Test the connection
  5. Bind to endpoints

Via API

curl -X POST http://localhost:9081/api/services \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Main Database",
    "service_type": "postgres",
    "config": {
      "host": "db.example.com",
      "port": 5432,
      "database": "myapp",
      "username": "app_user",
      "password": "secret"
    }
  }'

Using Services in Handlers

Services are accessed through typed handles in your handler code.

Database Example

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    let db = DbPool { pool_id: "main-db".to_string() };
    
    // Query
    let result = db.query(
        "SELECT id, name FROM users WHERE active = ?",
        &["true"]
    );
    
    match result {
        Ok(data) => Response::ok(json!({"users": data.rows})),
        Err(e) => Response::internal_error(e.to_string()),
    }
}
}

Redis Example

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    let redis = RedisPool { pool_id: "cache".to_string() };
    
    // Try cache first
    if let Ok(Some(cached)) = redis.get("user:123") {
        return Response::ok(json!({"source": "cache", "data": cached}));
    }
    
    // Cache miss - fetch and store
    let data = fetch_from_db();
    let _ = redis.setex("user:123", &data, 300); // Cache for 5 minutes
    
    Response::ok(json!({"source": "db", "data": data}))
}
}

Service Handles

DbPool

For SQL databases (PostgreSQL, MySQL, SQLite):

#![allow(unused)]
fn main() {
pub struct DbPool {
    pub pool_id: String,
}

impl DbPool {
    /// Execute a query, returns rows
    fn query(&self, sql: &str, params: &[&str]) -> Result<DbResult, HandlerError>;
    
    /// Execute a statement (INSERT, UPDATE, DELETE)
    fn execute(&self, sql: &str, params: &[&str]) -> Result<u64, HandlerError>;
}
}

RedisPool

For Redis:

#![allow(unused)]
fn main() {
pub struct RedisPool {
    pub pool_id: String,
}

impl RedisPool {
    /// Get a value
    fn get(&self, key: &str) -> Result<Option<String>, HandlerError>;
    
    /// Set a value
    fn set(&self, key: &str, value: &str) -> Result<(), HandlerError>;
    
    /// Set with expiration (seconds)
    fn setex(&self, key: &str, value: &str, seconds: u64) -> Result<(), HandlerError>;
}
}

Binding Services to Endpoints

Services must be bound to endpoints before they can be used:

  1. Create the service in the admin UI
  2. Open the endpoint configuration
  3. Add the service binding with a pool ID
  4. The pool ID is used in your handler code

This allows the same endpoint code to use different service instances in different environments (dev, staging, prod).

Next Steps

Storage Abstraction

A unified interface for storing data across different backends.

Overview

The Storage type provides a single API that works with:

  • Database backends - SQLite, PostgreSQL, MySQL
  • Object storage - MinIO, S3-compatible storage
  • File storage - FTP, FTPS, SFTP

This allows you to write handler code once and deploy it with different backends.

Creating a Storage Instance

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

// Database storage (SQLite, PostgreSQL, MySQL)
let db_storage = Storage::database("my-db-pool", "my_table");

// Object storage (MinIO, S3)
let obj_storage = Storage::object_storage("my-minio-pool", "data/items");

// File storage (FTP, SFTP)
let file_storage = Storage::file_storage("my-ftp-pool", "/data/items");
}

Storage Operations

Get by ID

#![allow(unused)]
fn main() {
let storage = Storage::database("pool", "users");

match storage.get("user-123") {
    Ok(Some(user)) => {
        // user is a JsonValue
        println!("Found: {}", user["name"]);
    }
    Ok(None) => {
        println!("Not found");
    }
    Err(e) => {
        eprintln!("Error: {}", e);
    }
}
}

List Records

#![allow(unused)]
fn main() {
// List all
let all_items = storage.list(None)?;

// List with filter (interpreted based on backend)
// For databases: WHERE status = ?
// For files: filter by filename pattern
let filtered = storage.list(Some("active"))?;
}

Create Record

#![allow(unused)]
fn main() {
let item = json!({
    "name": "Widget",
    "price": 29.99,
    "in_stock": true
});

storage.create("item-001", &item)?;
}

Update Record

#![allow(unused)]
fn main() {
let updated = json!({
    "name": "Widget Pro",
    "price": 39.99,
    "in_stock": true
});

storage.update("item-001", &updated)?;
}

Delete Record

#![allow(unused)]
fn main() {
match storage.delete("item-001")? {
    true => println!("Deleted"),
    false => println!("Not found"),
}
}

Backend Behavior

Database (SQLite, PostgreSQL, MySQL)

  • Records stored as table rows
  • table_name specifies the table
  • Filter parameter used in WHERE status = ?
  • Requires table to exist with proper schema

Object Storage (MinIO, S3)

  • Records stored as JSON files
  • Path: {base_path}/{id}.json
  • Filter parameter passed to listing API
  • Automatically creates bucket if needed

File Storage (FTP, SFTP)

  • Records stored as JSON files
  • Path: {base_path}/{id}.json
  • Filter parameter filters file listing
  • Directory must exist on server

Complete Example

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn get_storage() -> Storage {
    // Change this to switch backends:
    // Storage::database("pool", "items")
    // Storage::object_storage("minio", "items")
    Storage::file_storage("ftp", "/data/items")
}

fn handle(req: Request) -> Response {
    let storage = get_storage();
    
    match req.method.as_str() {
        "GET" => {
            if let Some(id) = req.path_param("id") {
                // Get single item
                match storage.get(id) {
                    Ok(Some(item)) => Response::ok(item),
                    Ok(None) => Response::not_found(),
                    Err(e) => e.to_response(),
                }
            } else {
                // List all items
                match storage.list(req.query_param("status").map(|s| s.as_str())) {
                    Ok(items) => Response::ok(json!({"items": items})),
                    Err(e) => e.to_response(),
                }
            }
        }
        "POST" => {
            let data: JsonValue = req.json().unwrap();
            let id = format!("item-{}", generate_id());
            match storage.create(&id, &data) {
                Ok(()) => Response::created(json!({"id": id})),
                Err(e) => e.to_response(),
            }
        }
        _ => Response::json(405, json!({"error": "Method not allowed"}))
    }
}

handler_loop!(handle);
}

See Also

Database Service

Connect to SQL databases (PostgreSQL, MySQL, SQLite) from your handlers.

Configuration

PostgreSQL

{
  "service_type": "postgres",
  "config": {
    "host": "localhost",
    "port": 5432,
    "database": "myapp",
    "username": "app_user",
    "password": "secret",
    "ssl_mode": "prefer",
    "pool_size": 10
  }
}

MySQL

{
  "service_type": "mysql",
  "config": {
    "host": "localhost",
    "port": 3306,
    "database": "myapp",
    "username": "app_user",
    "password": "secret",
    "use_ssl": false,
    "pool_size": 10
  }
}

SQLite

{
  "service_type": "sqlite",
  "config": {
    "path": "/data/app.db",
    "create_if_missing": true
  }
}

Usage

Basic Query

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    let db = DbPool { pool_id: "main".to_string() };
    
    let result = db.query("SELECT * FROM users WHERE active = ?", &["true"]);
    
    match result {
        Ok(data) => Response::ok(json!({"users": data.rows})),
        Err(e) => Response::internal_error(e.to_string()),
    }
}
}

Query with Parameters

Always use parameterized queries to prevent SQL injection:

#![allow(unused)]
fn main() {
// GOOD - parameterized
db.query("SELECT * FROM users WHERE id = ?", &[&user_id])

// BAD - string concatenation (SQL injection risk!)
// db.query(&format!("SELECT * FROM users WHERE id = {}", user_id), &[])
}

Insert, Update, Delete

Use execute for statements that modify data:

#![allow(unused)]
fn main() {
fn create_user(db: &DbPool, name: &str, email: &str) -> Result<u64, HandlerError> {
    db.execute(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        &[name, email]
    )
}

fn update_user(db: &DbPool, id: &str, name: &str) -> Result<u64, HandlerError> {
    db.execute(
        "UPDATE users SET name = ? WHERE id = ?",
        &[name, id]
    )
}

fn delete_user(db: &DbPool, id: &str) -> Result<u64, HandlerError> {
    db.execute("DELETE FROM users WHERE id = ?", &[id])
}
}

Working with Results

The DbResult contains rows as JSON objects:

#![allow(unused)]
fn main() {
let result = db.query("SELECT id, name, email FROM users", &[])?;

// result.rows is Vec<HashMap<String, Value>>
for row in &result.rows {
    let id = row.get("id");
    let name = row.get("name");
    println!("User: {:?} - {:?}", id, name);
}

// Or serialize the whole result
Response::ok(json!({
    "users": result.rows,
    "count": result.rows.len(),
}))
}

Typed Results

Parse rows into your own structs:

#![allow(unused)]
fn main() {
#[derive(Deserialize)]
struct User {
    id: i64,
    name: String,
    email: String,
}

fn get_user(db: &DbPool, id: &str) -> Result<Option<User>, HandlerError> {
    let result = db.query("SELECT * FROM users WHERE id = ?", &[id])?;
    
    if let Some(row) = result.rows.first() {
        let user: User = serde_json::from_value(row.clone().into())
            .map_err(|e| HandlerError::Internal(e.to_string()))?;
        Ok(Some(user))
    } else {
        Ok(None)
    }
}
}

Error Handling

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let db = DbPool { pool_id: "main".to_string() };
    
    let result = db.query("SELECT * FROM users", &[]);
    
    match result {
        Ok(data) => Response::ok(json!({"users": data.rows})),
        Err(HandlerError::DatabaseError(msg)) => {
            eprintln!("Database error: {}", msg);
            Response::internal_error("Database temporarily unavailable")
        }
        Err(HandlerError::ServiceUnavailable(msg)) => {
            Response::json(503, json!({"error": "Service unavailable", "retry_after": 5}))
        }
        Err(e) => e.to_response(),
    }
}
}

Best Practices

  1. Always use parameterized queries - Never concatenate user input into SQL
  2. Handle connection errors gracefully - Services may be temporarily unavailable
  3. Use appropriate pool sizes - Match your concurrency needs
  4. Keep queries simple - Complex logic is better in your handler code
  5. Log errors - Use eprintln! for debugging database issues

Redis Service

Use Redis for caching, sessions, and fast key-value storage.

Configuration

{
  "service_type": "redis",
  "config": {
    "host": "localhost",
    "port": 6379,
    "password": null,
    "database": 0,
    "use_tls": false,
    "pool_size": 10
  }
}

Configuration Options

FieldTypeDefaultDescription
hoststringrequiredRedis server hostname
portu166379Redis server port
passwordstringnullRedis password (optional)
databaseu80Redis database number (0-15)
use_tlsboolfalseEnable TLS encryption
pool_sizeu3210Connection pool size
usernamestringnullUsername for Redis 6+ ACL

Usage

Basic Operations

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    let redis = RedisPool { pool_id: "cache".to_string() };
    
    // Get a value
    match redis.get("my-key") {
        Ok(Some(value)) => Response::ok(json!({"value": value})),
        Ok(None) => Response::not_found(),
        Err(e) => Response::internal_error(e.to_string()),
    }
}
}

Set Values

#![allow(unused)]
fn main() {
// Set without expiration
redis.set("key", "value")?;

// Set with expiration (seconds)
redis.setex("session:abc123", &session_data, 3600)?; // 1 hour
redis.setex("rate:user:123", "1", 60)?; // 1 minute
}

Caching Pattern

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let redis = RedisPool { pool_id: "cache".to_string() };
    let db = DbPool { pool_id: "main".to_string() };
    
    let user_id = req.path_param("id").unwrap();
    let cache_key = format!("user:{}", user_id);
    
    // Try cache first
    if let Ok(Some(cached)) = redis.get(&cache_key) {
        return Response::ok(json!({
            "source": "cache",
            "user": serde_json::from_str::<JsonValue>(&cached).unwrap()
        }));
    }
    
    // Cache miss - fetch from database
    let result = db.query("SELECT * FROM users WHERE id = ?", &[user_id])?;
    
    if let Some(user) = result.rows.first() {
        // Cache for 5 minutes
        let user_json = serde_json::to_string(user).unwrap();
        let _ = redis.setex(&cache_key, &user_json, 300);
        
        return Response::ok(json!({
            "source": "database",
            "user": user
        }));
    }
    
    Response::not_found()
}
}

Session Management

#![allow(unused)]
fn main() {
fn get_session(redis: &RedisPool, session_id: &str) -> Result<Option<Session>, HandlerError> {
    let key = format!("session:{}", session_id);
    
    match redis.get(&key)? {
        Some(data) => {
            let session: Session = serde_json::from_str(&data)
                .map_err(|e| HandlerError::Internal(e.to_string()))?;
            Ok(Some(session))
        }
        None => Ok(None),
    }
}

fn save_session(redis: &RedisPool, session_id: &str, session: &Session) -> Result<(), HandlerError> {
    let key = format!("session:{}", session_id);
    let data = serde_json::to_string(session)
        .map_err(|e| HandlerError::Internal(e.to_string()))?;
    
    // Sessions expire in 24 hours
    redis.setex(&key, &data, 86400)
}

#[derive(Serialize, Deserialize)]
struct Session {
    user_id: String,
    created_at: String,
    data: JsonValue,
}
}

Rate Limiting

#![allow(unused)]
fn main() {
fn check_rate_limit(redis: &RedisPool, client_ip: &str) -> Result<bool, HandlerError> {
    let key = format!("rate:{}", client_ip);
    
    match redis.get(&key)? {
        Some(count) => {
            let count: u32 = count.parse().unwrap_or(0);
            if count >= 100 {
                return Ok(false); // Rate limited
            }
            // Note: This is a simplified example
            // Real implementation would use INCR command
            redis.setex(&key, &(count + 1).to_string(), 60)?;
            Ok(true)
        }
        None => {
            redis.setex(&key, "1", 60)?;
            Ok(true)
        }
    }
}

fn handle(req: Request) -> Response {
    let redis = RedisPool { pool_id: "cache".to_string() };
    
    let client_ip = req.client_ip.as_deref().unwrap_or("unknown");
    
    match check_rate_limit(&redis, client_ip) {
        Ok(true) => {
            // Process request normally
            Response::ok(json!({"status": "ok"}))
        }
        Ok(false) => {
            Response::json(429, json!({"error": "Too many requests"}))
                .with_header("Retry-After", "60")
        }
        Err(e) => {
            // If Redis is down, allow the request (fail open)
            eprintln!("Rate limit check failed: {}", e);
            Response::ok(json!({"status": "ok"}))
        }
    }
}
}

Error Handling

#![allow(unused)]
fn main() {
match redis.get("key") {
    Ok(Some(value)) => { /* use value */ }
    Ok(None) => { /* key doesn't exist */ }
    Err(HandlerError::RedisError(msg)) => {
        eprintln!("Redis error: {}", msg);
        // Fallback behavior
    }
    Err(HandlerError::ServiceUnavailable(_)) => {
        // Redis is down - decide on fallback
    }
    Err(e) => { /* other error */ }
}
}

Best Practices

  1. Use meaningful key prefixes - user:123, session:abc, cache:posts:1
  2. Always set expiration for cache keys - Prevents unbounded memory growth
  3. Handle Redis unavailability - Decide on fail-open vs fail-closed
  4. Don't store large values - Redis works best with small, fast lookups
  5. Use JSON for structured data - Easy to serialize/deserialize

FTP/SFTP Service

Transfer files to and from remote servers using FTP, FTPS, or SFTP.

Configuration

FTP (Unencrypted)

{
  "service_type": "ftp",
  "config": {
    "host": "ftp.example.com",
    "port": 21,
    "username": "user",
    "password": "secret",
    "protocol": "ftp",
    "base_path": "/uploads",
    "passive_mode": true,
    "timeout_seconds": 30
  }
}

FTPS (FTP over TLS)

{
  "service_type": "ftp",
  "config": {
    "host": "ftp.example.com",
    "port": 21,
    "username": "user",
    "password": "secret",
    "protocol": "ftps",
    "passive_mode": true
  }
}

SFTP (SSH File Transfer)

{
  "service_type": "ftp",
  "config": {
    "host": "sftp.example.com",
    "port": 22,
    "username": "user",
    "password": "secret",
    "protocol": "sftp",
    "base_path": "/home/user/uploads"
  }
}

Or with SSH key authentication:

{
  "service_type": "ftp",
  "config": {
    "host": "sftp.example.com",
    "port": 22,
    "username": "user",
    "private_key_path": "/path/to/id_rsa",
    "protocol": "sftp"
  }
}

Configuration Options

FieldTypeDefaultDescription
hoststringrequiredFTP server hostname
portu1621/22Server port (21 for FTP/FTPS, 22 for SFTP)
usernamestringrequiredLogin username
passwordstringnullLogin password
private_key_pathstringnullPath to SSH private key (SFTP only)
protocolstring"ftp"Protocol: "ftp", "ftps", or "sftp"
base_pathstringnullDefault directory on server
passive_modebooltrueUse passive mode (FTP/FTPS only)
timeout_secondsu3230Connection timeout

Usage

Upload a File

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    let ftp = FtpPool { pool_id: "uploads".to_string() };
    
    // Upload content
    let content = req.body.as_ref().unwrap();
    let result = ftp.put("/reports/daily.csv", content.as_bytes());
    
    match result {
        Ok(()) => Response::ok(json!({"uploaded": true})),
        Err(e) => Response::internal_error(e.to_string()),
    }
}
}

Download a File

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let ftp = FtpPool { pool_id: "files".to_string() };
    
    let filename = req.path_param("filename").unwrap();
    let path = format!("/data/{}", filename);
    
    match ftp.get(&path) {
        Ok(content) => Response::new(200)
            .with_header("Content-Type", "application/octet-stream")
            .with_body(content),
        Err(e) => Response::not_found(),
    }
}
}

List Directory

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let ftp = FtpPool { pool_id: "files".to_string() };
    
    match ftp.list("/reports") {
        Ok(files) => Response::ok(json!({"files": files})),
        Err(e) => Response::internal_error(e.to_string()),
    }
}
}

Use Cases

  • File uploads - Accept user uploads and store on FTP server
  • Report distribution - Upload generated reports to partner SFTP servers
  • Data import - Download files from vendor FTP for processing
  • Backup - Archive data to remote storage
  • Legacy integration - Connect to systems that only support FTP

Security Notes

  1. Prefer SFTP - Uses SSH encryption, most secure option
  2. Use FTPS if SFTP unavailable - TLS encryption for FTP
  3. Avoid plain FTP - Credentials sent in cleartext
  4. Use SSH keys - More secure than passwords for SFTP
  5. Restrict base_path - Limit access to specific directories

Email (SMTP) Service

Send emails from your handlers using SMTP.

Configuration

Basic SMTP with STARTTLS

{
  "service_type": "email",
  "config": {
    "host": "smtp.example.com",
    "port": 587,
    "username": "sender@example.com",
    "password": "app-password",
    "encryption": "starttls",
    "from_address": "noreply@example.com",
    "from_name": "My App",
    "reply_to": "support@example.com"
  }
}

Gmail SMTP

{
  "service_type": "email",
  "config": {
    "host": "smtp.gmail.com",
    "port": 587,
    "username": "your-email@gmail.com",
    "password": "your-app-password",
    "encryption": "starttls",
    "from_address": "your-email@gmail.com",
    "from_name": "Your Name"
  }
}

Implicit TLS (Port 465)

{
  "service_type": "email",
  "config": {
    "host": "smtp.example.com",
    "port": 465,
    "username": "sender@example.com",
    "password": "secret",
    "encryption": "tls",
    "from_address": "noreply@example.com"
  }
}

Local SMTP (No Auth)

{
  "service_type": "email",
  "config": {
    "host": "localhost",
    "port": 25,
    "encryption": "none",
    "from_address": "app@localhost"
  }
}

Configuration Options

FieldTypeDefaultDescription
hoststringrequiredSMTP server hostname
portu16587SMTP port
usernamestringnullAuth username (usually email address)
passwordstringnullAuth password or app password
encryptionstring"starttls"Encryption: "none", "starttls", or "tls"
from_addressstringrequiredDefault sender email
from_namestringnullDefault sender display name
reply_tostringnullDefault reply-to address
timeout_secondsu3230Connection timeout
max_retriesu323Send retry attempts

Usage

Send a Simple Email

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    let email = EmailPool { pool_id: "notifications".to_string() };
    
    let result = email.send(
        "user@example.com",
        "Welcome!",
        "Thanks for signing up.",
    );
    
    match result {
        Ok(()) => Response::ok(json!({"sent": true})),
        Err(e) => Response::internal_error(e.to_string()),
    }
}
}

Send HTML Email

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let email = EmailPool { pool_id: "notifications".to_string() };
    
    let html = r#"
        <h1>Welcome!</h1>
        <p>Thanks for joining us.</p>
        <a href="https://example.com/verify">Verify your email</a>
    "#;
    
    let result = email.send_html(
        "user@example.com",
        "Welcome to Our App",
        html,
    );
    
    match result {
        Ok(()) => Response::ok(json!({"sent": true})),
        Err(e) => Response::internal_error(e.to_string()),
    }
}
}

Send with Custom From

#![allow(unused)]
fn main() {
let result = email.send_from(
    "support@example.com",  // From
    "Support Team",         // From name
    "user@example.com",     // To
    "Your Ticket #123",     // Subject
    "We've received your support request...",
);
}

Common Providers

ProviderHostPortEncryption
Gmailsmtp.gmail.com587starttls
Outlooksmtp.office365.com587starttls
SendGridsmtp.sendgrid.net587starttls
Mailgunsmtp.mailgun.org587starttls
Amazon SESemail-smtp.{region}.amazonaws.com587starttls

Best Practices

  1. Use app passwords - Don't use your main account password
  2. Set up SPF/DKIM - Improve deliverability
  3. Handle failures - Emails can fail; log and retry
  4. Rate limit - Don't spam; respect provider limits
  5. Use templates - Consistent formatting
  6. Test with sandbox - Use test mode before production

Hello World

The simplest possible handler.

Code

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(_req: Request) -> Response {
    Response::ok(json!({
        "message": "Hello, World!"
    }))
}

handler_loop!(handle);
}

Endpoint Configuration

SettingValue
Path/hello
MethodGET
Domain*

Test

curl http://localhost:9080/hello

Response

{
  "message": "Hello, World!"
}

Variations

With Request Info

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    Response::ok(json!({
        "message": "Hello, World!",
        "method": req.method,
        "path": req.path,
        "request_id": req.request_id,
    }))
}
}

Plain Text

#![allow(unused)]
fn main() {
fn handle(_req: Request) -> Response {
    Response::text(200, "Hello, World!")
}
}

HTML

#![allow(unused)]
fn main() {
fn handle(_req: Request) -> Response {
    Response::new(200)
        .with_header("Content-Type", "text/html")
        .with_body("<h1>Hello, World!</h1>")
}
}

JSON API

Build a RESTful JSON API endpoint.

Code

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

#[derive(Deserialize)]
struct CreateItem {
    name: String,
    description: Option<String>,
    price: f64,
}

#[derive(Serialize)]
struct Item {
    id: String,
    name: String,
    description: Option<String>,
    price: f64,
    created_at: String,
}

fn handle(req: Request) -> Response {
    // Parse request body
    let input: CreateItem = match req.json() {
        Ok(data) => data,
        Err(e) => return Response::bad_request(format!("Invalid JSON: {}", e)),
    };
    
    // Validate
    if input.name.is_empty() {
        return Response::bad_request("Name is required");
    }
    
    if input.price < 0.0 {
        return Response::bad_request("Price must be non-negative");
    }
    
    // Create item (in real app, save to database)
    let item = Item {
        id: uuid::Uuid::new_v4().to_string(),
        name: input.name,
        description: input.description,
        price: input.price,
        created_at: chrono::Utc::now().to_rfc3339(),
    };
    
    // Return 201 Created
    Response::created(item)
}

handler_loop!(handle);
}

Endpoint Configuration

SettingValue
Path/items
MethodPOST
Domain*

Test

curl -X POST http://localhost:9080/items \
  -H "Content-Type: application/json" \
  -d '{"name": "Widget", "description": "A useful widget", "price": 19.99}'

Response

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Widget",
  "description": "A useful widget",
  "price": 19.99,
  "created_at": "2024-01-15T10:30:00.000Z"
}

Full CRUD Example

For a complete CRUD API, create multiple endpoints:

GET /items - List Items

#![allow(unused)]
fn main() {
fn handle(_req: Request) -> Response {
    // In real app, fetch from database
    let items = vec![
        json!({"id": "1", "name": "Item 1"}),
        json!({"id": "2", "name": "Item 2"}),
    ];
    
    Response::ok(json!({
        "items": items,
        "count": items.len(),
    }))
}
}

GET /items/{id} - Get Item

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let id = match req.path_param("id") {
        Some(id) => id,
        None => return Response::bad_request("Missing ID"),
    };
    
    // In real app, fetch from database
    if id == "1" {
        Response::ok(json!({
            "id": "1",
            "name": "Item 1",
            "price": 9.99,
        }))
    } else {
        Response::not_found()
    }
}
}

PUT /items/{id} - Update Item

#![allow(unused)]
fn main() {
#[derive(Deserialize)]
struct UpdateItem {
    name: Option<String>,
    price: Option<f64>,
}

fn handle(req: Request) -> Response {
    let id = match req.path_param("id") {
        Some(id) => id.clone(),
        None => return Response::bad_request("Missing ID"),
    };
    
    let update: UpdateItem = match req.json() {
        Ok(data) => data,
        Err(e) => return Response::bad_request(format!("Invalid JSON: {}", e)),
    };
    
    // In real app, update in database
    Response::ok(json!({
        "id": id,
        "name": update.name.unwrap_or("Unchanged".to_string()),
        "updated": true,
    }))
}
}

DELETE /items/{id} - Delete Item

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let id = match req.path_param("id") {
        Some(id) => id,
        None => return Response::bad_request("Missing ID"),
    };
    
    // In real app, delete from database
    eprintln!("Deleted item: {}", id);
    
    Response::no_content()
}
}

Path Parameters

Extract dynamic values from URL paths.

Basic Example

Endpoint Path: /users/{id}

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    // Extract the {id} parameter
    let user_id = match req.path_param("id") {
        Some(id) => id,
        None => return Response::bad_request("Missing user ID"),
    };
    
    Response::ok(json!({
        "user_id": user_id,
        "message": format!("Fetching user {}", user_id),
    }))
}

handler_loop!(handle);
}

Test

curl http://localhost:9080/users/123

Response

{
  "user_id": "123",
  "message": "Fetching user 123"
}

Multiple Parameters

Endpoint Path: /users/{user_id}/posts/{post_id}

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let user_id = req.path_param("user_id");
    let post_id = req.path_param("post_id");
    
    match (user_id, post_id) {
        (Some(uid), Some(pid)) => {
            Response::ok(json!({
                "user_id": uid,
                "post_id": pid,
            }))
        }
        _ => Response::bad_request("Missing parameters"),
    }
}
}

Test

curl http://localhost:9080/users/42/posts/7

Response

{
  "user_id": "42",
  "post_id": "7"
}

Type Conversion

Path parameters are always strings. Convert them to other types:

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let id_str = req.path_param("id")
        .ok_or("Missing ID")?;
    
    // Parse to integer
    let id: i64 = match id_str.parse() {
        Ok(n) => n,
        Err(_) => return Response::bad_request("ID must be a number"),
    };
    
    // Parse to UUID
    let uuid_str = req.path_param("uuid")
        .ok_or("Missing UUID")?;
    
    let uuid = match uuid::Uuid::parse_str(uuid_str) {
        Ok(u) => u,
        Err(_) => return Response::bad_request("Invalid UUID format"),
    };
    
    Response::ok(json!({
        "id": id,
        "uuid": uuid.to_string(),
    }))
}
}

Optional Parameters with Defaults

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    // Get page number, default to 1
    let page: u32 = req.path_param("page")
        .and_then(|p| p.parse().ok())
        .unwrap_or(1);
    
    Response::ok(json!({"page": page}))
}
}

Route Patterns

PatternMatchesParameters
/users/{id}/users/123id: "123"
/api/{version}/items/api/v2/itemsversion: "v2"
/files/{path}/files/docspath: "docs"
/{org}/{repo}/issues/{num}/acme/proj/issues/42org: "acme", repo: "proj", num: "42"

Common Patterns

Resource by ID

#![allow(unused)]
fn main() {
// GET /users/{id}
fn get_user(req: Request) -> Response {
    let id = req.path_param("id").unwrap();
    // Fetch user by ID
    Response::ok(json!({"id": id, "name": "John"}))
}
}

Nested Resources

#![allow(unused)]
fn main() {
// GET /organizations/{org_id}/teams/{team_id}/members
fn get_team_members(req: Request) -> Response {
    let org_id = req.path_param("org_id").unwrap();
    let team_id = req.path_param("team_id").unwrap();
    
    Response::ok(json!({
        "organization": org_id,
        "team": team_id,
        "members": ["alice", "bob"],
    }))
}
}

Slug-based Routes

#![allow(unused)]
fn main() {
// GET /blog/{slug}
fn get_blog_post(req: Request) -> Response {
    let slug = req.path_param("slug").unwrap();
    
    // Lookup by slug
    Response::ok(json!({
        "slug": slug,
        "title": format!("Post: {}", slug),
    }))
}
}

Query Parameters

Access URL query string values.

Basic Example

Endpoint Path: /search

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    // Get query parameter
    let query = req.query_param("q")
        .map(|s| s.to_string())
        .unwrap_or_default();
    
    if query.is_empty() {
        return Response::bad_request("Missing search query");
    }
    
    Response::ok(json!({
        "query": query,
        "results": [],
    }))
}

handler_loop!(handle);
}

Test

curl "http://localhost:9080/search?q=rust"

Response

{
  "query": "rust",
  "results": []
}

Pagination Example

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    // Parse pagination parameters with defaults
    let page: u32 = req.query_param("page")
        .and_then(|p| p.parse().ok())
        .unwrap_or(1);
    
    let limit: u32 = req.query_param("limit")
        .and_then(|l| l.parse().ok())
        .unwrap_or(10)
        .min(100);  // Cap at 100
    
    let offset = (page - 1) * limit;
    
    Response::ok(json!({
        "page": page,
        "limit": limit,
        "offset": offset,
        "items": [],
    }))
}
}

Test

curl "http://localhost:9080/items?page=2&limit=20"

Response

{
  "page": 2,
  "limit": 20,
  "offset": 20,
  "items": []
}

Filtering Example

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    // Get filter parameters
    let status = req.query_param("status");
    let category = req.query_param("category");
    let min_price: Option<f64> = req.query_param("min_price")
        .and_then(|p| p.parse().ok());
    let max_price: Option<f64> = req.query_param("max_price")
        .and_then(|p| p.parse().ok());
    
    Response::ok(json!({
        "filters": {
            "status": status,
            "category": category,
            "price_range": {
                "min": min_price,
                "max": max_price,
            },
        },
        "items": [],
    }))
}
}

Test

curl "http://localhost:9080/products?status=active&category=electronics&min_price=100"

Sorting Example

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let sort_by = req.query_param("sort")
        .map(|s| s.to_string())
        .unwrap_or_else(|| "created_at".to_string());
    
    let order = req.query_param("order")
        .map(|s| s.to_string())
        .unwrap_or_else(|| "desc".to_string());
    
    // Validate sort field
    let valid_fields = ["name", "created_at", "price", "popularity"];
    if !valid_fields.contains(&sort_by.as_str()) {
        return Response::bad_request(format!(
            "Invalid sort field. Valid options: {:?}", valid_fields
        ));
    }
    
    Response::ok(json!({
        "sort": {
            "field": sort_by,
            "order": order,
        },
        "items": [],
    }))
}
}

Boolean Parameters

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    // Parse boolean parameters
    let include_deleted = req.query_param("include_deleted")
        .map(|v| v == "true" || v == "1")
        .unwrap_or(false);
    
    let verbose = req.query_param("verbose")
        .map(|v| v == "true" || v == "1")
        .unwrap_or(false);
    
    Response::ok(json!({
        "include_deleted": include_deleted,
        "verbose": verbose,
    }))
}
}

All Query Parameters

Access all query parameters at once:

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    // Log all query parameters
    for (key, value) in &req.query {
        eprintln!("Query param: {} = {}", key, value);
    }
    
    Response::ok(json!({
        "query_params": req.query,
    }))
}
}

Validation Helper

Create a reusable validation function:

#![allow(unused)]
fn main() {
fn parse_pagination(req: &Request) -> Result<(u32, u32), Response> {
    let page: u32 = req.query_param("page")
        .and_then(|p| p.parse().ok())
        .unwrap_or(1);
    
    if page == 0 {
        return Err(Response::bad_request("Page must be >= 1"));
    }
    
    let limit: u32 = req.query_param("limit")
        .and_then(|l| l.parse().ok())
        .unwrap_or(10);
    
    if limit > 100 {
        return Err(Response::bad_request("Limit must be <= 100"));
    }
    
    Ok((page, limit))
}

fn handle(req: Request) -> Response {
    let (page, limit) = match parse_pagination(&req) {
        Ok(p) => p,
        Err(response) => return response,
    };
    
    Response::ok(json!({"page": page, "limit": limit}))
}
}

Error Handling

Robust error handling patterns for handlers.

Basic Pattern

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn handle(req: Request) -> Response {
    match process_request(&req) {
        Ok(data) => Response::ok(data),
        Err(e) => e.to_response(),
    }
}

fn process_request(req: &Request) -> Result<JsonValue, HandlerError> {
    let input: CreateUser = req.json()
        .map_err(|e| HandlerError::ValidationError(e.to_string()))?;
    
    // Process and return result
    Ok(json!({"id": "123", "name": input.name}))
}

#[derive(Deserialize)]
struct CreateUser {
    name: String,
}

handler_loop!(handle);
}

Error Types and Status Codes

#![allow(unused)]
fn main() {
fn process(req: &Request) -> Result<JsonValue, HandlerError> {
    // 400 Bad Request - Invalid input
    if req.body.is_none() {
        return Err(HandlerError::ValidationError("Body required".into()));
    }
    
    // 401 Unauthorized - Missing/invalid auth
    if req.header("Authorization").is_none() {
        return Err(HandlerError::Unauthorized("Token required".into()));
    }
    
    // 404 Not Found - Resource doesn't exist
    let user = find_user("123");
    if user.is_none() {
        return Err(HandlerError::NotFound("User not found".into()));
    }
    
    // 503 Service Unavailable - Backend down
    if !database_available() {
        return Err(HandlerError::ServiceUnavailable("Database down".into()));
    }
    
    // 500 Internal Error - Unexpected error
    if something_broke() {
        return Err(HandlerError::Internal("Unexpected error".into()));
    }
    
    Ok(json!({"status": "ok"}))
}
}

Input Validation

#![allow(unused)]
fn main() {
#[derive(Deserialize)]
struct RegisterUser {
    email: String,
    password: String,
    name: String,
}

fn validate_input(input: &RegisterUser) -> Result<(), HandlerError> {
    // Email validation
    if !input.email.contains('@') {
        return Err(HandlerError::ValidationError(
            "Invalid email format".into()
        ));
    }
    
    // Password validation
    if input.password.len() < 8 {
        return Err(HandlerError::ValidationError(
            "Password must be at least 8 characters".into()
        ));
    }
    
    // Name validation
    if input.name.trim().is_empty() {
        return Err(HandlerError::ValidationError(
            "Name is required".into()
        ));
    }
    
    Ok(())
}

fn handle(req: Request) -> Response {
    let input: RegisterUser = match req.json() {
        Ok(i) => i,
        Err(e) => return Response::bad_request(format!("Invalid JSON: {}", e)),
    };
    
    if let Err(e) = validate_input(&input) {
        return e.to_response();
    }
    
    Response::created(json!({"email": input.email}))
}
}

Custom Error Type

#![allow(unused)]
fn main() {
enum AppError {
    UserNotFound(String),
    EmailTaken(String),
    InvalidCredentials,
    RateLimited,
    DatabaseError(String),
}

impl From<AppError> for HandlerError {
    fn from(e: AppError) -> Self {
        match e {
            AppError::UserNotFound(id) => 
                HandlerError::NotFound(format!("User {} not found", id)),
            AppError::EmailTaken(email) => 
                HandlerError::ValidationError(format!("Email {} already registered", email)),
            AppError::InvalidCredentials => 
                HandlerError::Unauthorized("Invalid email or password".into()),
            AppError::RateLimited => 
                HandlerError::Internal("Rate limit exceeded".into()),
            AppError::DatabaseError(msg) => 
                HandlerError::DatabaseError(msg),
        }
    }
}

fn process(req: &Request) -> Result<JsonValue, AppError> {
    // Business logic with custom errors
    Err(AppError::InvalidCredentials)
}

fn handle(req: Request) -> Response {
    match process(&req) {
        Ok(data) => Response::ok(data),
        Err(e) => HandlerError::from(e).to_response(),
    }
}
}

Logging Errors

Always log errors for debugging:

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    match process(&req) {
        Ok(data) => Response::ok(data),
        Err(e) => {
            // Log with request ID for tracing
            eprintln!("[{}] Error: {}", req.request_id, e);
            
            // Log stack trace for internal errors
            if matches!(e, HandlerError::Internal(_) | HandlerError::DatabaseError(_)) {
                eprintln!("[{}] Request path: {}", req.request_id, req.path);
                eprintln!("[{}] Request body: {:?}", req.request_id, req.body);
            }
            
            e.to_response()
        }
    }
}
}

Graceful Degradation

Handle service failures gracefully:

#![allow(unused)]
fn main() {
fn handle(req: Request) -> Response {
    let redis = RedisPool { pool_id: "cache".to_string() };
    let db = DbPool { pool_id: "main".to_string() };
    
    // Try cache first
    let cached = redis.get("data:key");
    
    match cached {
        Ok(Some(data)) => {
            return Response::ok(json!({"source": "cache", "data": data}));
        }
        Ok(None) => { /* Cache miss, continue */ }
        Err(e) => {
            // Log but don't fail - Redis being down shouldn't break the app
            eprintln!("Redis error (non-fatal): {}", e);
        }
    }
    
    // Fallback to database
    match db.query("SELECT * FROM data", &[]) {
        Ok(result) => Response::ok(json!({"source": "db", "data": result.rows})),
        Err(e) => {
            eprintln!("Database error: {}", e);
            Response::json(503, json!({
                "error": "Service temporarily unavailable",
                "retry_after": 5,
            }))
        }
    }
}
}

Pet Store Demo

A complete REST API example demonstrating the same Pet Store API working with multiple storage backends.

Overview

The Pet Store demo shows how Rust Edge Gateway's Storage abstraction allows the same handler code to work with:

  • SQLite - Embedded database (no external dependencies)
  • PostgreSQL - Full-featured relational database
  • MySQL - Popular relational database
  • MinIO - Object storage (pets stored as JSON files)
  • FTP/SFTP - File transfer (pets stored as JSON files)

Quick Start

cd examples/petstore

# SQLite (default - no external dependencies)
./setup.sh sqlite

# PostgreSQL
./setup.sh postgres

# MySQL
./setup.sh mysql

# MinIO (object storage)
./setup.sh minio

# FTP/SFTP (file storage)
./setup.sh ftp

API Endpoints

MethodPathDescription
GET/petsList all pets (optional ?status= filter)
POST/petsCreate a new pet
GET/pets/{petId}Get a pet by ID
PUT/pets/{petId}Update a pet
DELETE/pets/{petId}Delete a pet

Storage Abstraction

The key to multi-backend support is the Storage type:

#![allow(unused)]
fn main() {
use rust_edge_gateway_sdk::prelude::*;

fn get_storage() -> Storage {
    // Choose one:
    Storage::database("petstore", "pets")           // SQLite/PostgreSQL/MySQL
    Storage::object_storage("petstore", "pets")     // MinIO/S3
    Storage::file_storage("petstore", "pets")       // FTP/SFTP
}

fn handle(req: Request) -> Response {
    let storage = get_storage();
    
    match storage.list(None) {
        Ok(pets) => Response::ok(json!({"pets": pets})),
        Err(e) => e.to_response(),
    }
}
}

Storage API

All storage backends implement the same interface:

#![allow(unused)]
fn main() {
// Get a record by ID
storage.get("pet-123") -> Result<Option<JsonValue>, HandlerError>

// List all records (with optional filter)
storage.list(Some("available")) -> Result<Vec<JsonValue>, HandlerError>

// Create a new record
storage.create("pet-123", &pet_json) -> Result<(), HandlerError>

// Update an existing record
storage.update("pet-123", &pet_json) -> Result<(), HandlerError>

// Delete a record
storage.delete("pet-123") -> Result<bool, HandlerError>
}

Database Schema

For SQL backends, use the provided schema:

CREATE TABLE IF NOT EXISTS pets (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    category TEXT,
    tags TEXT,  -- JSON array as string
    status TEXT NOT NULL DEFAULT 'available',
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_pets_status ON pets(status);

File Storage Format

For MinIO and FTP backends, pets are stored as individual JSON files:

/pets/
  pet-001.json
  pet-002.json
  pet-003.json

Each file contains:

{
  "id": "pet-001",
  "name": "Buddy",
  "category": "dog",
  "tags": ["friendly", "trained"],
  "status": "available",
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2024-01-15T10:30:00Z"
}

Handler Code

See the handler implementations in examples/petstore/handlers/:

  • list_pets.rs - List with optional status filter
  • get_pet.rs - Get by ID
  • create_pet.rs - Create with validation
  • update_pet.rs - Partial update support
  • delete_pet.rs - Delete by ID

Testing

# Create a pet
curl -X POST http://petstore.example.com/pets \
  -H "Content-Type: application/json" \
  -d '{"name": "Buddy", "category": "dog", "status": "available"}'

# List all pets
curl http://petstore.example.com/pets

# Filter by status
curl http://petstore.example.com/pets?status=available

# Get a specific pet
curl http://petstore.example.com/pets/pet-001

# Update a pet
curl -X PUT http://petstore.example.com/pets/pet-001 \
  -H "Content-Type: application/json" \
  -d '{"status": "sold"}'

# Delete a pet
curl -X DELETE http://petstore.example.com/pets/pet-001

Management API

The Rust Edge Gateway provides a REST API for managing endpoints, domains, collections, and services.

Base URL

http://localhost:9081/api

In production, access via your admin domain:

https://rust-edge-gateway.yourdomain.com/api

Response Format

All API responses follow this format:

{
  "ok": true,
  "data": { ... }
}

Or for errors:

{
  "ok": false,
  "error": "Error message"
}

Authentication

Currently, the API is open. Future versions will support authentication.

Rate Limiting

No rate limiting is currently applied to the management API.

Endpoints Overview

ResourceEndpoints
Domains/api/domains/*
Collections/api/collections/*
Services/api/services/*
Endpoints/api/endpoints/*
Import/api/import/*
System/api/health, /api/stats

System Endpoints

Health Check

Check if the gateway is running.

GET /api/health

Response:

{
  "ok": true,
  "data": {
    "status": "healthy",
    "version": "0.1.0"
  }
}

Statistics

Get gateway statistics.

GET /api/stats

Response:

{
  "ok": true,
  "data": {
    "endpoints_total": 10,
    "endpoints_running": 8,
    "requests_handled": 1234,
    "uptime_seconds": 3600
  }
}

Import Endpoints

Import OpenAPI Spec

Create endpoints from an OpenAPI 3.x specification.

POST /api/import/openapi
Content-Type: application/json

{
  "spec": "openapi: 3.0.0\ninfo:\n  title: Pet Store\n...",
  "domain": "api.example.com",
  "domain_id": "uuid-of-domain",
  "create_collection": true
}

Request Body:

FieldTypeRequiredDescription
specstringYesOpenAPI YAML or JSON content
domainstringYesDomain to associate endpoints with
domain_idstringNo*Domain UUID (*required if create_collection is true)
collection_idstringNoExisting collection to add endpoints to
create_collectionboolNoCreate new collection from spec info

Response:

{
  "ok": true,
  "data": {
    "collection": {
      "id": "uuid",
      "name": "Pet Store",
      "base_path": "/v1"
    },
    "endpoints_created": 5,
    "endpoints": [
      {"id": "uuid", "name": "getPets", "path": "/pets", "method": "GET"},
      {"id": "uuid", "name": "createPet", "path": "/pets", "method": "POST"}
    ]
  }
}

Import Bundle (ZIP)

Upload a ZIP file containing an OpenAPI spec and handler code files.

POST /api/import/bundle?domain=api.example.com&create_collection=true&domain_id=uuid&compile=true&start=true
Content-Type: multipart/form-data

# Form field: bundle (or file, zip) = your-bundle.zip

Query Parameters:

ParameterTypeRequiredDescription
domainstringYesDomain to associate endpoints with
domain_idstringNo*Domain UUID (*required if create_collection is true)
collection_idstringNoExisting collection to add endpoints to
create_collectionboolNoCreate new collection from spec info
compileboolNoCompile handlers after import
startboolNoStart handlers after compilation (requires compile=true)

Bundle Structure:

bundle.zip
β”œβ”€β”€ openapi.yaml          # OpenAPI spec (or openapi.json, api.yaml, spec.yaml)
└── handlers/             # Handler files (can also be at root or in src/)
    β”œβ”€β”€ get_pets.rs       # Matches operationId "getPets" or "get_pets"
    β”œβ”€β”€ create_pet.rs     # Matches operationId "createPet" or "create_pet"
    └── get_pet_by_id.rs  # Matches operationId "getPetById" or "get_pet_by_id"

Handler files are matched to OpenAPI operations by normalizing names:

  • getPet.rs β†’ matches operationId getPet or get_pet
  • list_all_pets.rs β†’ matches operationId listAllPets or list_all_pets

Response:

{
  "ok": true,
  "data": {
    "collection": {"id": "uuid", "name": "Pet Store"},
    "endpoints_created": 5,
    "endpoints_updated": 0,
    "handlers_matched": 5,
    "compiled": 5,
    "started": 5,
    "endpoints": [...],
    "errors": []
  }
}

Example with curl:

curl -X POST "http://localhost:8081/api/import/bundle?domain=api.example.com&create_collection=true&domain_id=abc123&compile=true&start=true" \
  -F "bundle=@my-api.zip"

Common Patterns

List with Filters

Most list endpoints support query parameters:

GET /api/endpoints?domain=api.example.com&enabled=true

Pagination

List endpoints will support pagination in future versions:

GET /api/endpoints?page=1&limit=20

Error Handling

Always check the ok field in responses:

const response = await fetch('/api/endpoints');
const data = await response.json();

if (data.ok) {
  console.log('Endpoints:', data.data);
} else {
  console.error('Error:', data.error);
}

Domains API

Domains represent the top-level organization for your endpoints (e.g., api.example.com).

List Domains

GET /api/domains

Response:

{
  "ok": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Production API",
      "host": "api.example.com",
      "description": "Main production API",
      "enabled": true,
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z"
    }
  ]
}

Create Domain

POST /api/domains
Content-Type: application/json

{
  "name": "Production API",
  "host": "api.example.com",
  "description": "Main production API",
  "enabled": true
}

Request Body:

FieldTypeRequiredDescription
namestringYesDisplay name for the domain
hoststringYesHostname (e.g., api.example.com)
descriptionstringNoOptional description
enabledboolNoWhether domain is active (default: true)

Response:

{
  "ok": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Production API",
    "host": "api.example.com",
    "description": "Main production API",
    "enabled": true,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Get Domain

GET /api/domains/{id}

Response:

{
  "ok": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Production API",
    "host": "api.example.com",
    "description": "Main production API",
    "enabled": true,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Update Domain

PUT /api/domains/{id}
Content-Type: application/json

{
  "name": "Updated API Name",
  "description": "Updated description",
  "enabled": false
}

Request Body:

All fields are optional. Only provided fields will be updated.

FieldTypeDescription
namestringDisplay name
hoststringHostname
descriptionstringDescription
enabledboolActive status

Response:

{
  "ok": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Updated API Name",
    "host": "api.example.com",
    "description": "Updated description",
    "enabled": false,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T11:00:00Z"
  }
}

Delete Domain

DELETE /api/domains/{id}

Response:

{
  "ok": true,
  "data": null
}

Note: Deleting a domain will also delete all associated collections and endpoints.

Get Domain Collections

List all collections belonging to a domain.

GET /api/domains/{id}/collections

Response:

{
  "ok": true,
  "data": [
    {
      "id": "collection-uuid",
      "domain_id": "domain-uuid",
      "name": "Pet Store",
      "description": "Pet management endpoints",
      "base_path": "/pets",
      "enabled": true,
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z"
    }
  ]
}

Collections API

Collections group related endpoints within a domain (e.g., "Pet Store", "User Management").

List Collections

GET /api/collections

Query Parameters:

ParameterTypeDescription
domain_idstringFilter by domain UUID

Response:

{
  "ok": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "domain_id": "domain-uuid",
      "name": "Pet Store",
      "description": "Pet management endpoints",
      "base_path": "/pets",
      "enabled": true,
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z"
    }
  ]
}

Create Collection

POST /api/collections
Content-Type: application/json

{
  "domain_id": "domain-uuid",
  "name": "Pet Store",
  "description": "Pet management endpoints",
  "base_path": "/pets",
  "enabled": true
}

Request Body:

FieldTypeRequiredDescription
domain_idstringYesParent domain UUID
namestringYesDisplay name
descriptionstringNoOptional description
base_pathstringNoCommon path prefix for endpoints
enabledboolNoWhether collection is active (default: true)

Response:

{
  "ok": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "domain_id": "domain-uuid",
    "name": "Pet Store",
    "description": "Pet management endpoints",
    "base_path": "/pets",
    "enabled": true,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Get Collection

GET /api/collections/{id}

Response:

{
  "ok": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "domain_id": "domain-uuid",
    "name": "Pet Store",
    "description": "Pet management endpoints",
    "base_path": "/pets",
    "enabled": true,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Update Collection

PUT /api/collections/{id}
Content-Type: application/json

{
  "name": "Updated Name",
  "description": "Updated description",
  "base_path": "/v2/pets",
  "enabled": false
}

Request Body:

All fields are optional. Only provided fields will be updated.

FieldTypeDescription
namestringDisplay name
descriptionstringDescription
base_pathstringPath prefix
enabledboolActive status

Response:

{
  "ok": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "domain_id": "domain-uuid",
    "name": "Updated Name",
    "description": "Updated description",
    "base_path": "/v2/pets",
    "enabled": false,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T11:00:00Z"
  }
}

Delete Collection

DELETE /api/collections/{id}

Response:

{
  "ok": true,
  "data": null
}

Note: Deleting a collection will also delete all associated endpoints.

Get Collection Endpoints

List all endpoints in a collection.

GET /api/collections/{id}/endpoints

Response:

{
  "ok": true,
  "data": [
    {
      "id": "endpoint-uuid",
      "name": "getPets",
      "path": "/pets",
      "method": "GET",
      "domain": "api.example.com",
      "collection_id": "collection-uuid",
      "description": "List all pets",
      "enabled": true
    }
  ]
}

Services API

Services represent backend connections (databases, caches, storage) that handlers can use.

List Services

GET /api/services

Response:

{
  "ok": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Main Database",
      "service_type": "postgres",
      "config": {
        "host": "db.example.com",
        "port": 5432,
        "database": "myapp"
      },
      "enabled": true,
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z"
    }
  ]
}

Create Service

POST /api/services
Content-Type: application/json

{
  "name": "Main Database",
  "service_type": "postgres",
  "config": {
    "host": "db.example.com",
    "port": 5432,
    "database": "myapp",
    "username": "app_user",
    "password": "secret"
  },
  "enabled": true
}

Request Body:

FieldTypeRequiredDescription
namestringYesDisplay name
service_typestringYesType of service (see below)
configobjectYesService-specific configuration
enabledboolNoWhether service is active (default: true)

Service Types:

TypeDescription
sqliteSQLite embedded database
postgresPostgreSQL database
mysqlMySQL database
redisRedis cache/store
mongodbMongoDB document database
minioMinIO/S3 object storage
memcachedMemcached cache
ftpFTP/FTPS/SFTP file transfer
emailSMTP email sending

Response:

{
  "ok": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Main Database",
    "service_type": "postgres",
    "config": { ... },
    "enabled": true,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Get Service

GET /api/services/{id}

Update Service

PUT /api/services/{id}
Content-Type: application/json

{
  "name": "Updated Name",
  "config": {
    "host": "new-db.example.com"
  },
  "enabled": false
}

Delete Service

DELETE /api/services/{id}

Test Service Connection

Test if the service is reachable and properly configured.

POST /api/services/{id}/test

Response (Success):

{
  "ok": true,
  "data": {
    "connected": true,
    "latency_ms": 5,
    "message": "Connection successful"
  }
}

Response (Failure):

{
  "ok": true,
  "data": {
    "connected": false,
    "error": "Connection refused"
  }
}

Service Configuration Examples

PostgreSQL

{
  "service_type": "postgres",
  "config": {
    "host": "localhost",
    "port": 5432,
    "database": "myapp",
    "username": "app_user",
    "password": "secret",
    "ssl_mode": "prefer",
    "pool_size": 10
  }
}

MySQL

{
  "service_type": "mysql",
  "config": {
    "host": "localhost",
    "port": 3306,
    "database": "myapp",
    "username": "app_user",
    "password": "secret",
    "use_ssl": false,
    "pool_size": 10
  }
}

Redis

{
  "service_type": "redis",
  "config": {
    "host": "localhost",
    "port": 6379,
    "password": null,
    "database": 0,
    "use_tls": false,
    "pool_size": 10
  }
}

SQLite

{
  "service_type": "sqlite",
  "config": {
    "path": "/data/app.db",
    "create_if_missing": true
  }
}

MinIO

{
  "service_type": "minio",
  "config": {
    "endpoint": "minio.example.com:9000",
    "access_key": "minioadmin",
    "secret_key": "minioadmin",
    "use_ssl": true,
    "bucket": "uploads"
  }
}

FTP/SFTP

{
  "service_type": "ftp",
  "config": {
    "host": "sftp.example.com",
    "port": 22,
    "username": "user",
    "password": "secret",
    "protocol": "sftp",
    "base_path": "/uploads",
    "timeout_seconds": 30
  }
}

Email (SMTP)

{
  "service_type": "email",
  "config": {
    "host": "smtp.example.com",
    "port": 587,
    "username": "sender@example.com",
    "password": "app-password",
    "encryption": "starttls",
    "from_address": "noreply@example.com",
    "from_name": "My App"
  }
}

Endpoints API

Endpoints are the core resource - each represents a route with handler code.

List Endpoints

GET /api/endpoints

Query Parameters:

ParameterTypeDescription
domainstringFilter by domain hostname
collection_idstringFilter by collection UUID
enabledboolFilter by enabled status

Response:

{
  "ok": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "getPets",
      "path": "/pets",
      "method": "GET",
      "domain": "api.example.com",
      "collection_id": "collection-uuid",
      "description": "List all pets",
      "code": "use rust_edge_gateway_sdk::prelude::*;\n...",
      "enabled": true,
      "status": "running",
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z"
    }
  ]
}

Create Endpoint

POST /api/endpoints
Content-Type: application/json

{
  "name": "getPets",
  "path": "/pets",
  "method": "GET",
  "domain": "api.example.com",
  "collection_id": "collection-uuid",
  "description": "List all pets",
  "code": "use rust_edge_gateway_sdk::prelude::*;\n\nfn handle(req: Request) -> Response {\n    Response::ok(json!({\"pets\": []}))\n}\n\nhandler_loop!(handle);",
  "enabled": true
}

Request Body:

FieldTypeRequiredDescription
namestringYesEndpoint name (for display)
pathstringYesURL path pattern (e.g., /pets/{id})
methodstringYesHTTP method (GET, POST, PUT, DELETE, PATCH)
domainstringYesDomain hostname or * for all
collection_idstringNoParent collection UUID
descriptionstringNoDescription of the endpoint
codestringNoRust handler code
enabledboolNoWhether endpoint is active (default: true)

Response:

{
  "ok": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "getPets",
    "path": "/pets",
    "method": "GET",
    "domain": "api.example.com",
    "collection_id": "collection-uuid",
    "description": "List all pets",
    "code": "...",
    "enabled": true,
    "status": "created",
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Get Endpoint

GET /api/endpoints/{id}

Update Endpoint

PUT /api/endpoints/{id}
Content-Type: application/json

{
  "name": "Updated Name",
  "code": "// new code...",
  "enabled": false
}

Delete Endpoint

DELETE /api/endpoints/{id}

Compile Endpoint

Compile the handler code into an executable.

POST /api/endpoints/{id}/compile

Response (Success):

{
  "ok": true,
  "data": {
    "status": "compiled",
    "message": "Compilation successful"
  }
}

Response (Failure):

{
  "ok": false,
  "error": "error[E0308]: mismatched types\n  --> src/main.rs:5:5\n..."
}

Start Endpoint

Start the worker process for this endpoint.

POST /api/endpoints/{id}/start

Response:

{
  "ok": true,
  "data": {
    "status": "running",
    "pid": 12345
  }
}

Stop Endpoint

Stop the worker process.

POST /api/endpoints/{id}/stop

Response:

{
  "ok": true,
  "data": {
    "status": "stopped"
  }
}

Endpoint Status Values

StatusDescription
createdEndpoint defined, not yet compiled
compiledCode compiled successfully
runningWorker process is active
stoppedWorker stopped, can be restarted
errorCompilation or runtime error

Bind Service to Endpoint

Associate a service with an endpoint.

POST /api/endpoints/{id}/services
Content-Type: application/json

{
  "service_id": "service-uuid",
  "pool_id": "main-db"
}

Request Body:

FieldTypeRequiredDescription
service_idstringYesService UUID to bind
pool_idstringYesIdentifier used in handler code

Unbind Service

DELETE /api/endpoints/{endpoint_id}/services/{service_id}