Back to flin
flin

Inyección de contexto de solicitud

Cómo FLIN inyecta params, query, body, headers, cookies y session en cada manejador de ruta automáticamente -- cero importaciones, cero boilerplate, cero ceremonia.

Juste Thales Gnimavo & Claude | March 26, 2026 7 min flin
EN/ FR/ ES
flinrust

In Express.js, every handler receives (req, res, next). In Django, every view receives request. In FastAPI, you declare parameters with type annotations and the framework injects them. In Go, you pass http.ResponseWriter and *http.Request to every handler function. Every framework requires you to declare, receive, and destructure the request context.

FLIN takes a different approach. The request context is injected implicitly. Inside any route handler or middleware, the variables params, query, body, headers, cookies, session, and request are simply available. No function parameters. No imports. No type declarations. They exist because you are inside an HTTP context, and FLIN knows that.

Las variables de contexto integradas

Every route handler and middleware in FLIN has access to these variables:

flin// Available in every route handler and middleware

params       // URL parameters from dynamic segments
query        // Query string parameters
body         // Parsed request body (JSON, form, multipart)
headers      // Request headers (case-insensitive)
cookies      // Request cookies
session      // Session data (read/write)
request      // Full request object (method, path, ip, etc.)

These are not global variables. They are scoped to the current request. Two concurrent requests each see their own params, their own body, their own session. The isolation is guaranteed by the runtime.

Cómo funciona: el alcance de la solicitud

When the FLIN runtime dispatches a request to a handler, it creates a new execution scope with the context variables pre-populated:

rustfn execute_handler(
    vm: &mut VirtualMachine,
    handler: &CompiledFunction,
    ctx: &RequestContext,
) -> Result<Value, RuntimeError> {
    // Create new scope for this request
    let scope = vm.push_scope();

    // Inject context variables
    scope.set("params", ctx.params.to_flin_value());
    scope.set("query", ctx.query.to_flin_value());
    scope.set("body", ctx.body.to_flin_value());
    scope.set("headers", ctx.headers.to_flin_value());
    scope.set("cookies", ctx.cookies.to_flin_value());
    scope.set("session", ctx.session.to_flin_value());
    scope.set("request", ctx.to_flin_value());

    // Execute handler bytecode
    let result = vm.execute(handler)?;

    // Pop scope (context variables are freed)
    vm.pop_scope();

    Ok(result)
}

The scope is pushed before execution and popped after. Each request gets a clean set of context variables. There is no possibility of leaking data between requests because the scope lifetime is tied to the request lifetime.

Using params

URL parameters from dynamic route segments are available through params:

flin// app/api/users/[id].flin
// Request: GET /api/users/42

route GET {
    user_id = params.id    // "42" (always a string)
    user = User.find(to_int(user_id))
    user
}
flin// app/products/[category]/[id].flin
// Request: GET /products/electronics/789

route GET {
    category = params.category   // "electronics"
    product_id = params.id       // "789"

    product = Product.where(category == category).find(to_int(product_id))
    product
}

Parameters are always strings because they come from URL segments. Use to_int(), to_float(), or other conversion functions when you need typed values. The validate block handles this conversion automatically.

Using query

Query string parameters are available through query:

flin// Request: GET /api/products?page=2&per_page=20&category=shoes&sort=price

route GET {
    page = to_int(query.page || "1")
    per_page = to_int(query.per_page || "20")
    category = query.category        // "shoes" or "" if missing
    sort_field = query.sort || "created_at"

    products = Product
    if category != "" {
        products = products.where(category == category)
    }
    products.order(sort_field).limit(per_page).offset((page - 1) * per_page)
}

Missing query parameters return an empty string, not null or undefined. This eliminates the null-checking boilerplate that every other framework requires:

javascript// Express.js: defensive coding required
const page = parseInt(req.query.page) || 1;
const category = req.query.category || '';
if (category && typeof category === 'string') { ... }
flin// FLIN: query parameters are always strings, never null
page = to_int(query.page || "1")
category = query.category
if category != "" { ... }

Using body

The parsed request body is available through body, as covered in detail in the previous article. The key point for context injection is that body adapts to the content type automatically:

flin// JSON request: body is a map with typed values
route POST {
    name = body.name           // text
    age = body.age             // int
    tags = body.tags           // [text]
}

// Form request: body is a map with string values
route POST {
    name = body.name           // text (always)
    age = to_int(body.age)     // convert from text
}

// Multipart request: body has both fields and files
route POST {
    title = body.title         // text field
    file = body.avatar         // file object
}

Using headers

Request headers are available through headers with case-insensitive key access:

flin// Request headers:
// Authorization: Bearer eyJ...
// Accept-Language: fr-FR
// X-Custom-Header: some-value

route GET {
    auth = headers["Authorization"]        // "Bearer eyJ..."
    lang = headers["Accept-Language"]      // "fr-FR"
    custom = headers["x-custom-header"]    // "some-value" (case-insensitive)
}

Case-insensitive access is implemented by normalizing header names to lowercase during parsing. This prevents the common bug where code checks for Authorization but the client sends authorization.

Using cookies

Request cookies are available through cookies:

flinroute GET {
    theme = cookies["theme"] || "light"
    locale = cookies["locale"] || "en"
    tracking_id = cookies["_tid"]
}

Setting response cookies is done through the response object or through helper functions:

flinroute POST {
    set_cookie("theme", body.theme, {
        max_age: 365 * 24 * 60 * 60,   // 1 year
        path: "/",
        secure: true,
        httponly: false                   // Accessible to JS for theme
    })

    { success: true }
}

Using session

The session is a persistent key-value store scoped to the user's browser. It is backed by an encrypted cookie and survives across requests:

flin// Reading session data
user_email = session.user           // "" if not set
user_name = session.userName        // "" if not set
user_id = session.userId            // "" if not set

// Writing session data
session.user = found.email
session.userName = found.name
session.userId = to_text(found.id)

// Clearing session data
session.user = none
session.userName = none
session.userId = none

Session values are always strings. This is a deliberate design constraint. Storing complex objects in sessions leads to serialization bugs, versioning problems, and security vulnerabilities. Strings are simple, serializable, and predictable.

Using request

The request object provides metadata about the raw HTTP request:

flinroute GET {
    method = request.method      // "GET"
    path = request.path          // "/api/users/42"
    ip = request.ip              // "192.168.1.100"
    protocol = request.protocol  // "https"
    host = request.host          // "example.com"
    user_agent = request.user_agent  // "Mozilla/5.0 ..."
}

The request object is read-only in route handlers but writable in middleware. This allows middleware to attach computed values that downstream handlers can access:

flin// _middleware.flin
middleware {
    token = headers["Authorization"]
    if token != "" {
        claims = verify_token(token)
        request.user = claims
    }
    next()
}

// api/profile.flin
route GET {
    // request.user was set by middleware
    if request.user == none {
        return error(401, "Not authenticated")
    }
    User.find(request.user.sub)
}

Por qué la inyección implícita

The decision to inject context implicitly rather than requiring explicit parameter declarations was controversial. Explicit is usually better than implicit -- it is a core principle of good API design. So why did we choose implicit injection?

FLIN is a domain-specific language for web applications. Every FLIN route handler runs in an HTTP context. There is no scenario where a route handler does not have access to params or body. Making developers declare these explicitly would add ceremony without adding information.

Consistency with the rest of FLIN. FLIN entities are available by name without imports. Built-in functions like save, delete, and hash_password are available without imports. Making HTTP context variables require special declaration would be inconsistent.

Reduced boilerplate for beginners. FLIN is designed to be accessible to developers building their first web application. Forcing them to understand dependency injection, parameter binding, or type-annotated extractors before they can read a query parameter would raise the barrier to entry.

The trade-off is discoverability. A new developer reading a FLIN file might wonder where params comes from. The answer is documented, consistent, and always the same: these variables are available in every HTTP context. Once you learn them once, you know them forever.

En el próximo artículo, explore FLIN's middleware system -- how _middleware.flin files create a composable pipeline of request processing that replaces the ad-hoc middleware patterns of Express, Koa, and their descendants.


Esta es la Parte 100 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abiyán y un CTO de IA diseñaron y construyeron un lenguaje de programación desde cero.

Navegación de la serie: - [98] Rutas API: backend y frontend en un solo archivo - [99] Análisis automático de JSON y cuerpos de formulario - [100] Inyección de contexto de solicitud (estás aquí) - [101] El sistema de middleware

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude deblo

El prompt decía que lo anunciara. El modelo no lo hizo. Así que lo hizo el worker: cerrar un silencio de 15 segundos en Déblo Voice

Déblo Voice se quedaba en silencio unos 15 segundos cada vez que una pregunta difícil activaba una búsqueda profunda. El prompt ya exigía un anuncio, pero el modelo de audio nativo emite la llamada a la función como su primer token. Además, el silencio tenía dos partes, la ejecución de la herramienta y la latencia de la respuesta, así que ningún prompt podía cerrarlo. Llevamos la garantía al worker de LiveKit: un anuncio pregrabado con la voz y el idioma de la sesión, y un suave sonido de tecleo mientras el agente piensa. Probado en producción el 16 de octubre de 2026: 14 y 17,5 segundos de silencio del lado del modelo, unos 0,4 segundos percibidos por el usuario.

13 min Oct 16, 2026
deblodeblo-voicevoice-aigemini-live +17
Thales & Claude zerosuite

El servidor que no dimos de baja: vaciar una máquina de producción en una noche, y cada número que comprobamos

Una noche, seis aplicaciones migradas fila por fila, tres copias de seguridad vacías, una sin destino y un servidor cuyo futuro cambió tres veces.

19 min Oct 2, 2026
caspclaude-codeclaude-opus-5.5infrastructure +10
Thales & Claude zerosuite

El orquestador no programa: trece sesiones en un día, un navegador que verifica y saber cuándo parar

Un día, una plataforma privada de un cliente, trece sesiones headless de Claude encadenadas por un orquestador que nunca escribe código. Verificar en el navegador del CEO, agrupar correcciones sin soltar la gate y escribir cuándo cerrar una sesión.

19 min Sep 30, 2026
caspclaude-codeclaude-opus-5.5multi-session +12