Back to flin
flin

Response Helpers and Status Codes

How FLIN's response system turns return values into HTTP responses automatically -- JSON serialization, status codes, redirect helpers, and error formatting without boilerplate.

Thales & Claude | March 25, 2026 8 min flin
flinresponsehttpstatus-codes

In Express.js, sending a JSON response requires res.status(201).json({ user }). In Django, you construct JsonResponse(data, status=201). In FastAPI, you return a dict and decorate the function with @app.post(status_code=201). Every framework has its own API for the same fundamental operation: turning application data into an HTTP response.

FLIN simplifies this to its logical minimum. The last expression in a route block IS the response. If it is an object, it becomes JSON with a 200 status. If it is a response{} literal, you control the status and headers. If it is an error() call, you get a formatted error response. If you call redirect(), the client is sent elsewhere. Four patterns cover every HTTP response you will ever need.

Pattern 1: Implicit JSON Response

The simplest and most common pattern. The last expression in a route block is automatically serialized to JSON:

flin// Returns 200 OK with JSON body
route GET {
    User.all
}
// Response: [{"id": 1, "name": "Thales", ...}, ...]

route GET {
    user = User.find(params.id)
    user
}
// Response: {"id": 42, "name": "Thales", "email": "[email protected]"}

route GET {
    {
        status: "healthy",
        uptime: server_uptime(),
        version: "1.0.0",
        database: "connected"
    }
}
// Response: {"status": "healthy", "uptime": 3600, ...}

The serialization follows predictable rules:

FLIN TypeJSON Output
Entity instanceObject with all visible fields
Entity listArray of objects
Map/object literalJSON object
TextJSON string
Int/FloatJSON number
BoolJSON boolean
NoneJSON null
ListJSON array

Fields marked with @hidden (like passwords) are automatically excluded from serialization. You never accidentally leak a password hash in an API response.

Pattern 2: Explicit Response

When you need to control the status code, headers, or response format, use the response{} literal:

flin// 201 Created with Location header
route POST {
    user = User { name: body.name, email: body.email }
    save user

    response {
        status: 201
        headers: {
            "Location": "/api/users/" + to_text(user.id)
        }
        body: user
    }
}

// 204 No Content
route DELETE {
    user = User.find(params.id)
    delete user

    response {
        status: 204
    }
}

// Custom content type
route GET {
    csv_data = generate_csv(User.all)

    response {
        status: 200
        headers: {
            "Content-Type": "text/csv",
            "Content-Disposition": "attachment; filename=\"users.csv\""
        }
        body: csv_data
    }
}

The response{} literal has three optional fields:

  • status -- HTTP status code (default: 200)
  • headers -- Map of response headers
  • body -- Response body (serialized to JSON if it is a FLIN value)

Pattern 3: Error Responses

The error() function generates a structured error response:

flinroute GET {
    user = User.find(params.id)
    if user == none {
        return error(404, "User not found")
    }
    user
}
// Error response: {"error": "User not found", "status": 404}

route POST {
    existing = User.where(email == body.email).first
    if existing != none {
        return error(409, "Email already registered")
    }
    // ...
}

route PUT {
    if body.quantity > product.stock {
        return error(422, "Insufficient stock", {
            available: product.stock,
            requested: body.quantity
        })
    }
    // ...
}
// Error response: {"error": "Insufficient stock", "status": 422,
//                  "details": {"available": 5, "requested": 10}}

The error() function accepts two or three arguments:

flinerror(status_code, message)
error(status_code, message, details)

The response always includes the error field (the message), the status field (the code), and optionally a details field (additional context). This consistent format means frontend code can always parse errors the same way.

Pattern 4: Redirects

The redirect() function sends a 302 Found response:

flinroute POST {
    // Process form...
    redirect("/dashboard")
}

// With specific status code
route POST {
    redirect("/new-location", 301)   // Permanent redirect
}

Redirects are commonly used in the authentication flow, where processing pages redirect to the dashboard on success or back to the login page on failure:

flinroute POST {
    if login_successful {
        session.user = user.email
        redirect("/tasks")
    } else {
        session.loginError = "Invalid credentials"
        redirect("/login")
    }
}

Validation Error Responses

When a validate block fails, FLIN automatically generates a 400 response with field-level error details:

flinroute POST {
    validate {
        name: text @required @minLength(2) @maxLength(100)
        email: text @required @email
        age: int @min(13) @max(120)
    }
    // ...
}

If a client sends {"name": "A", "email": "not-an-email", "age": 5}, the response is:

json{
    "error": "Validation failed",
    "status": 400,
    "fields": {
        "name": "Must be at least 2 characters",
        "email": "Must be a valid email address",
        "age": "Must be at least 13"
    }
}

This response format is generated entirely by the runtime. The developer writes the validate block and FLIN handles the error formatting. The field names match exactly what the client sent, making it straightforward for frontend code to display errors next to the correct form fields.

How Responses Are Built Internally

The FLIN runtime converts route block return values to HTTP responses through a straightforward dispatch:

rustfn value_to_response(value: Value) -> Response {
    match value {
        Value::Response(r) => {
            // Explicit response{} literal
            Response::new(r.status)
                .headers(r.headers)
                .json_body_if_value(r.body)
        }

        Value::Error(status, message, details) => {
            // error() function result
            let mut body = json!({
                "error": message,
                "status": status,
            });
            if let Some(d) = details {
                body["details"] = value_to_json(d);
            }
            Response::new(status)
                .header("Content-Type", "application/json")
                .body(body.to_string())
        }

        Value::Redirect(url, status) => {
            Response::new(status.unwrap_or(302))
                .header("Location", url)
        }

        Value::None => {
            // No return value -> 204 No Content
            Response::new(204)
        }

        other => {
            // Any other value -> 200 OK with JSON body
            let json = value_to_json(other);
            Response::new(200)
                .header("Content-Type", "application/json")
                .body(json.to_string())
        }
    }
}

The conversion is deterministic and exhaustive. Every possible FLIN value maps to exactly one HTTP response. There is no ambiguity, no default behavior that might surprise you, and no silent failure mode.

Security Headers on Every Response

Regardless of which response pattern you use, FLIN adds security headers to every HTTP response in production mode:

httpX-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
Permissions-Policy: camera=(), microphone=(), geolocation=()

These headers are not optional. They are not middleware you might forget to add. They are part of every response that leaves the server. A FLIN application is hardened against clickjacking, MIME sniffing, XSS, and information leakage by default.

Combining Patterns in Practice

A realistic API endpoint uses multiple response patterns depending on the outcome:

flin// app/api/orders.flin

guard auth

route POST {
    validate {
        product_id: int @required
        quantity: int @required @min(1) @max(100)
        shipping_address: text @required
    }

    product = Product.find(body.product_id)
    if product == none {
        return error(404, "Product not found")
    }

    if product.stock < body.quantity {
        return error(422, "Insufficient stock", {
            available: product.stock,
            requested: body.quantity
        })
    }

    order = Order {
        user_id: to_int(session.userId),
        product_id: product.id,
        quantity: body.quantity,
        total: product.price * body.quantity,
        shipping_address: body.shipping_address,
        status: "pending"
    }
    save order

    product.stock = product.stock - body.quantity
    save product

    response {
        status: 201
        headers: {
            "Location": "/api/orders/" + to_text(order.id)
        }
        body: order
    }
}

This single handler uses validation errors (400), not-found errors (404), business logic errors (422), and a successful creation response (201). Each response type is expressed clearly and handled consistently.

The Philosophy of Response Design

FLIN's response system reflects a core design principle: the common case should be effortless, and the uncommon case should be straightforward.

Returning JSON (the common case) requires no ceremony -- just return a value. Setting a custom status code (less common) requires a response{} literal. Returning an error (uncommon but important) requires an error() call. Redirecting (rare in API routes, common in auth flows) requires a redirect() call.

No framework-specific response classes to instantiate. No chain of method calls to build. No imported decorators or annotations. Just return what you want the client to receive, and FLIN handles the HTTP plumbing.

This concludes Arc 9 -- FLIN's web server and HTTP handling. Ten articles covering the embedded server, file-based routing, API routes, body parsing, context injection, middleware, guards, WebSockets, file uploads, and response handling. In Arc 10, we turn to security: how FLIN addresses the OWASP Top 10, implements Argon2 password hashing, JWT authentication, rate limiting, 2FA, OAuth2, and WhatsApp OTP -- all as built-in language features.


This is Part 105 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.

Series Navigation: - [103] WebSocket Support Built Into the Language - [104] File Upload Support - [105] Response Helpers and Status Codes (you are here) - [106] Security by Design: OWASP Top 10 in the Language

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles