Servidores HTTP en Gleam: construir una API con Wisp y Mist

Cuando se trata de servidores HTTP en Gleam, el stack habitual es Wisp más Mist. Wisp es el framework de alto nivel que gestiona rutas, requests y responses; Mist es el servidor HTTP subyacente escrito en Gleam que maneja las conexiones TCP. Los dos corren sobre la BEAM, así que heredan toda la concurrencia y tolerancia a fallos del ecosistema Erlang.

En este artículo construimos una API REST sencilla con Gleam, Wisp y Mist, viendo cómo se estructura un proyecto web real.

Crear el proyecto

gleam new mi_api
cd mi_api
gleam add wisp mist gleam_http gleam_json

El fichero gleam.toml resultante:

name = "mi_api"
version = "1.0.0"

[dependencies]
gleam_stdlib = ">= 0.34.0"
wisp = ">= 0.16.0"
mist = ">= 1.2.0"
gleam_http = ">= 3.6.0"
gleam_json = ">= 1.0.0"

El punto de entrada: arrancar Mist

La función main arranca el servidor Mist y le pasa el handler de Wisp:

import gleam/erlang/process
import mist
import wisp
import mi_api/router

pub fn main() {
  wisp.configure_logger()

  let secret_key_base = wisp.random_string(64)

  let assert Ok(_) =
    wisp.mist_handler(router.handle_request, secret_key_base)
    |> mist.new
    |> mist.port(8000)
    |> mist.start_http

  process.sleep_forever()
}

El router: gestionar rutas

El router recibe cada request y devuelve un response. Wisp no usa macros de enrutamiento: el routing es pattern matching sobre el método HTTP y el path segmentado:

import wisp.{type Request, type Response}
import gleam/http.{Get, Post, Delete}
import mi_api/handlers/users

pub fn handle_request(req: Request) -> Response {
  use req <- wisp.handle_head(req)

  case wisp.path_segments(req) {
    [] ->
      wisp.html_response(wisp.Text("Bienvenido a mi API"), 200)

    ["users"] if req.method == Get ->
      users.list_users(req)

    ["users"] if req.method == Post ->
      users.create_user(req)

    ["users", id] if req.method == Get ->
      users.get_user(req, id)

    ["users", id] if req.method == Delete ->
      users.delete_user(req, id)

    _ ->
      wisp.not_found()
  }
}

El pattern matching hace el routing explícito y legible. No hay magic strings, no hay DSL especial: es el mismo case que usarías en cualquier otra parte del código Gleam.

Handlers: leer el body y responder JSON

import wisp.{type Request, type Response}
import gleam/json
import gleam/dynamic

pub fn get_user(req: Request, id: String) -> Response {
  case find_user(id) {
    Ok(user) -> {
      let body = json.object([
        #("id", json.string(user.id)),
        #("name", json.string(user.name)),
        #("email", json.string(user.email)),
      ])
      wisp.json_response(json.to_string(body), 200)
    }
    Error(_) ->
      wisp.not_found()
  }
}

pub fn create_user(req: Request) -> Response {
  use body <- wisp.require_json(req)

  let decoder = dynamic.decode2(
    fn(name, email) { #(name, email) },
    dynamic.field("name", dynamic.string),
    dynamic.field("email", dynamic.string),
  )

  case decoder(body) {
    Ok(#(name, email)) -> {
      wisp.json_response("{"status":"created"}", 201)
    }
    Error(_) ->
      wisp.unprocessable_entity()
  }
}

Middleware: use y la función de continuación

Wisp usa un patrón de middleware con use y funciones de continuación. Esto permite encadenar transformaciones del request antes de llegar al handler principal:

pub fn handle_request(req: Request) -> Response {
  // Middleware de logging
  use req <- wisp.handle_head(req)
  // Middleware de autenticación
  use user <- require_auth(req)
  // A partir de aquí tenemos el user autenticado
  case wisp.path_segments(req) {
    ["profile"] -> get_profile(req, user)
    _ -> wisp.not_found()
  }
}

fn require_auth(req: Request, next: fn(User) -> Response) -> Response {
  case get_auth_header(req) {
    Ok(user) -> next(user)
    Error(_) -> wisp.response(401)
  }
}

Respuestas: tipos de response disponibles

Wisp ofrece funciones de conveniencia para los tipos de respuesta más habituales:

  • wisp.json_response(body, status) — respuesta con Content-Type: application/json
  • wisp.html_response(body, status) — respuesta HTML
  • wisp.not_found() — 404
  • wisp.unprocessable_entity() — 422
  • wisp.internal_server_error() — 500
  • wisp.redirect(url) — 302 redirect

Rendimiento y concurrencia

Al correr sobre la BEAM, cada request HTTP se maneja en su propio proceso ligero. No hay thread pool que configurar, no hay límite de conexiones concurrentes arbitrario: la máquina virtual gestiona miles de requests simultáneos de forma natural. Si un handler falla, el proceso muere pero el servidor sigue corriendo, y el supervisor puede reiniciarlo si es necesario.

Wisp no tiene la madurez de Phoenix todavía, pero para construir APIs REST es perfectamente funcional. Para proyectos donde la seguridad de tipos es importante, el enfoque de Gleam elimina toda una categoría de bugs que en frameworks más dinámicos solo se detectan en tests o en producción.

En el siguiente artículo de la serie vemos la gestión de errores en Gleam con Result y Option en profundidad, que es lo que usa Wisp internamente para manejar requests fallidos.

Imagen: Pexels / Digital Buggu

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP