Inicio / Elixir / Phoenix Framework: Web en Tiempo Real / Plugs y Middleware

Plugs y Middleware

Module plugs, function plugs, pipelines y custom plugs.

Intermedio
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Plugs y Middleware en Phoenix

Introducción

Los Plugs son el corazón del manejo de peticiones HTTP en Phoenix. Cada plug recibe una conexión (Plug.Conn), la transforma y la devuelve. Son componentes composables que forman el pipeline de procesamiento.

Plug.Conn

La estructura %Plug.Conn{} representa la conexión HTTP completa:

# Campos importantes de Plug.Conn
%Plug.Conn{
  host: "localhost",
  method: "GET",
  path_info: ["api", "usuarios"],
  request_path: "/api/usuarios",
  query_params: %{"page" => "1"},
  body_params: %{},
  assigns: %{},
  status: nil,
  resp_body: nil,
  halted: false
}

Podemos inspeccionar y modificar la conexión con funciones del módulo Plug.Conn:

conn
|> put_status(200)
|> put_resp_content_type("application/json")
|> assign(:usuario_actual, usuario)
|> send_resp(200, Jason.encode!(%{ok: true}))

Function Plugs

Los function plugs son funciones simples que reciben conn y opts:

defmodule MyAppWeb.ProductoController do
  use MyAppWeb, :controller

  plug :validar_admin when action in [:create, :update, :delete]

  def index(conn, _params) do
    render(conn, :index, productos: Catalogo.list_productos())
  end

  defp validar_admin(conn, _opts) do
    if conn.assigns[:usuario_actual] && conn.assigns.usuario_actual.rol == :admin do
      conn
    else
      conn
      |> put_status(:forbidden)
      |> put_view(json: MyAppWeb.ErrorJSON)
      |> render(:"403")
      |> halt()
    end
  end
end

Module Plugs: init/1 y call/2

Los module plugs implementan dos callbacks: init/1 (compilación) y call/2 (ejecución):

defmodule MyApp.Plugs.Locale do
  import Plug.Conn

  def init(default_locale), do: default_locale

  def call(conn, default_locale) do
    locale =
      conn
      |> get_req_header("accept-language")
      |> List.first()
      |> parse_locale()
      |> Kernel.||(default_locale)

    Gettext.put_locale(MyAppWeb.Gettext, locale)
    assign(conn, :locale, locale)
  end

  defp parse_locale(nil), do: nil
  defp parse_locale(header), do: header |> String.slice(0, 2)
end

Plug.Builder

Plug.Builder permite componer múltiples plugs en un módulo:

defmodule MyApp.Plugs.APISetup do
  use Plug.Builder

  plug Plug.Logger
  plug Plug.Parsers,
    parsers: [:json],
    json_decoder: Jason
  plug :set_formato

  defp set_formato(conn, _opts) do
    put_resp_content_type(conn, "application/json")
  end
end

Composición en Pipelines

Las pipelines del router componen plugs de forma declarativa:

pipeline :auth_api do
  plug :accepts, ["json"]
  plug MyApp.Plugs.AuthenticateToken
  plug MyApp.Plugs.Locale, "es"
  plug MyApp.Plugs.RateLimiter, max_requests: 100, window_ms: 60_000
end

scope "/api", MyAppWeb do
  pipe_through [:auth_api]
  resources "/cursos", CursoController
end

Custom Auth Plug

Un plug de autenticación basado en tokens Bearer:

defmodule MyApp.Plugs.AuthenticateToken do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
         {:ok, usuario} <- MyApp.Cuentas.verificar_token(token) do
      assign(conn, :usuario_actual, usuario)
    else
      _ ->
        conn
        |> put_status(:unauthorized)
        |> Phoenix.Controller.json(%{error: "Token inválido o ausente"})
        |> halt()
    end
  end
end

Rate Limiting Plug

Un plug para limitar la tasa de peticiones:

defmodule MyApp.Plugs.RateLimiter do
  import Plug.Conn

  def init(opts) do
    %{
      max_requests: Keyword.get(opts, :max_requests, 60),
      window_ms: Keyword.get(opts, :window_ms, 60_000)
    }
  end

  def call(conn, %{max_requests: max, window_ms: window}) do
    key = "rate_limit:#{client_ip(conn)}"

    case MyApp.RateStore.check_rate(key, max, window) do
      {:ok, count} ->
        conn
        |> put_resp_header("x-ratelimit-limit", "#{max}")
        |> put_resp_header("x-ratelimit-remaining", "#{max - count}")

      {:error, :rate_exceeded} ->
        conn
        |> put_status(:too_many_requests)
        |> Phoenix.Controller.json(%{error: "Límite de peticiones excedido"})
        |> halt()
    end
  end

  defp client_ip(conn), do: conn.remote_ip |> :inet.ntoa() |> to_string()
end

Resumen

Los Plugs son el mecanismo fundamental de Phoenix para procesar peticiones HTTP. Existen como funciones simples o módulos con init/call, se componen con Plug.Builder y pipelines del router. Permiten crear middleware reutilizable para autenticación, rate limiting, localización y cualquier transformación de la conexión.

🔒

Ejercicio práctico disponible

Creación de Plugs

Desbloquear ejercicios
// Creación de Plugs
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes