SkillAgentSearch skills...

Filtr

Query params validation in Phoenix

Install / Use

/learn @Blatts12/Filtr
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Filtr logo

Parameter validation library for Elixir with Phoenix integration.

Filtr provides a flexible, plugin-based system for validating and casting parameters in Phoenix applications. It offers seamless integration with Phoenix Controllers and LiveViews using an attr-style syntax similar to Phoenix Components.

Features

  • Phoenix Integration - Seamless Controller and LiveView support
  • Plugin System - Extend with custom types and validators
  • Multiple Error Modes - Fallback, strict, and raise modes with per-field overrides
  • Zero Dependencies - Lightweight core library
  • attr-like param macro - Familiar, declarative syntax just like for Phoenix Components
  • Nested Schemas - Deep nesting with param ... do...end macro syntax

Requirements

  • Elixir ~> 1.16
  • Phoenix >= 1.6.0 (optional, for Controller/LiveView integration)
  • Phoenix LiveView >= 0.20.0 (optional, for LiveView integration)

Installation

Add filtr to your list of dependencies in mix.exs:

def deps do
  [
    {:filtr, "~> 0.4.0"}
  ]
end

Formatter

To ensure proper formatting of the param macro, add :filtr to your .formatter.exs configuration:

[import_deps: [:filtr]]

Quick Start

Phoenix Controller

defmodule MyAppWeb.UserController do
  use MyAppWeb, :controller
  use Filtr.Controller, error_mode: :raise

  param :name, :string, required: true
  param :age, :integer, min: 18, max: 120
  param :email, :string, required: true, pattern: ~r/@/

  def create(conn, params) do
    # params.name is guaranteed to be a string
    # params.age is guaranteed to be an integer between 18 and 120
    # params.email is guaranteed to be a string containing "@"
    json(conn, %{message: "User #{params.name} created"})
  end

  # Nested parameters
  param :filters do
    param :q, :string, default: ""
    param :page, :integer, default: 1, min: 1
    param :category, :string, in: ["books", "movies", "music"], default: "books"
  end

  def search(conn, params) do
    json(conn, %{query: params.filters.q, page: params.filters.page})
  end

  # List of nested schema
  param :items, :list do
    param :name, :string, required: true
    param :quantity, :integer, min: 1, default: 1
  end

  def order(conn, params) do
    # params.items is a list
    json(conn, %{items: params.items})
  end
end

Phoenix LiveView

defmodule MyAppWeb.SearchLive do
  use MyAppWeb, :live_view
  use Filtr.LiveView, error_mode: :raise

  param :query, :string, required: true, min: 1
  param :limit, :integer, default: 10, min: 1, max: 100

  # Nested parameters for better organization
  param :filters do
    param :category, :string, default: "all"
    param :sort, :string, in: ["name", "date"], default: "name"
  end

  # List of nested schemas
  param :tags, :list do
    param :label, :string, required: true
    param :color, :string, in: ["red", "blue", "green"], default: "blue"
  end

  def mount(_params, _session, socket) do
    # socket.assigns.filtr contains validated params
    # socket.assigns.filtr.query - validated query string
    # socket.assigns.filtr.limit - validated limit (defaults to 10)
    # socket.assigns.filtr.filters.category - validated category
    # socket.assigns.filtr.filters.sort - validated sort
    # socket.assigns.filtr.tags - validated list of tag objects
    {:ok, socket}
  end

  def handle_params(_params, _uri, socket) do
    # Params are automatically revalidated on navigation
    {:noreply, socket}
  end
end

Standalone Usage

schema = %{
  name: [type: :string, validators: [required: true, min: 2]],
  age: [type: :integer, validators: [min: 18, max: 120]],
  tags: [type: {:list, :string}, validators: [max: 5]]
}

params = %{
  "name" => "John Doe",
  "age" => "25",
  "tags" => ["elixir", "phoenix"]
}

result = Filtr.run(schema, params, error_mode: :raise)
# %{name: "John Doe", age: 25, tags: ["elixir", "phoenix"]}

Error Modes

Filtr supports three error handling modes:

Fallback Mode (Default)

Returns default values or nil on validation errors:

schema = %{age: [type: :integer, validators: [default: 18]]}
params = %{"age" => "invalid"}

Filtr.run(schema, params, error_mode: :fallback)
# %{age: 18}

Strict Mode

Returns error tuples for invalid values:

schema = %{age: [type: :integer, validators: [min: 18]]}
params = %{"age" => "10"}

Filtr.run(schema, params, error_mode: :strict)
# %{age: {:error, ["must be at least 18"]}}

Raise Mode

Raises exceptions on validation errors:

schema = %{name: [type: :string, validators: [required: true]]}
params = %{}

Filtr.run(schema, params, error_mode: :raise)
# ** (RuntimeError) Invalid value for name: required

Global Configuration

Set a default error mode in your config:

# config/config.exs
config :filtr, error_mode: :strict

Or specify per-module:

use Filtr.Controller, error_mode: :strict
use Filtr.LiveView, error_mode: :raise

Validation Status

Filtr automatically includes a _valid? field in the result map to indicate whether all parameters passed validation. This field is always present regardless of error mode.

schema = %{
  name: [type: :string, validators: [required: true]],
  age: [type: :integer, validators: [min: 18]]
}

# Valid params
params = %{"name" => "John", "age" => "25"}
result = Filtr.run(schema, params, error_mode: :strict)
# %{name: "John", age: 25, _valid?: true}

# Invalid params
params = %{"age" => "10"}
result = Filtr.run(schema, params, error_mode: :strict)
# %{name: {:error, ["required"]}, age: {:error, [...]}, _valid?: false}

This makes it easy to check validation status without inspecting individual fields:

defmodule MyAppWeb.UserController do
  use Filtr.Controller, error_mode: :strict

  param :name, :string, required: true
  param :email, :string, required: true, pattern: ~r/@/

  def create(conn, params) do
    if params._valid? do
      # All params are valid, proceed with creation
      json(conn, %{message: "User #{params.name} created"})
    else
      # Some params have errors, collect and return them
      conn
      |> put_status(:bad_request)
      |> json(%{errors: collect_errors(params)})
    end
  end
end

Advanced Features

Nested Schemas

Filtr supports nested schemas with two syntaxes:

Macro Syntax (Controllers & LiveViews)

Use the param do...end syntax for clean, declarative nested schemas:

defmodule MyAppWeb.UserController do
  use Filtr.Controller

  param :user do
    param :name, :string, required: true
    param :email, :string, required: true, pattern: ~r/@/
    param :age, :integer, min: 18
  end

  param :settings do
    param :theme, :string, in: ["light", "dark"], default: "light"
    param :notifications, :boolean, default: true
  end

  def create(conn, params) do
    # params.user.name
    # params.user.email
    # params.settings.theme
    json(conn, %{message: "User #{params.user.name} created"})
  end
end

Deep nesting is fully supported:

param :company do
  param :name, :string, required: true

  param :headquarters do
    param :country, :string, default: "US"

    param :contact do
      param :email, :string, required: true
      param :phone, :string, default: ""
    end
  end
end

# Access: params.company.headquarters.contact.email

Map Syntax (Standalone)

For standalone usage, use the map syntax:

schema = %{
  user: %{
    name: [type: :string, validators: [required: true]],
    email: [type: :string, validators: [required: true]]
  },
  settings: %{
    theme: [type: :string, validators: [in: ["light", "dark"]]],
    notifications: [type: :boolean]
  }
}

params = %{
  "user" => %{
    "name" => "John",
    "email" => "john@example.com"
  },
  "settings" => %{
    "theme" => "dark",
    "notifications" => "true"
  }
}

result = Filtr.run(schema, params)
# %{
#   user: %{name: "John", email: "john@example.com"},
#   settings: %{theme: "dark", notifications: true}
# }

List of Nested Schemas

Filtr supports lists containing nested schema, allowing you to validate arrays of complex objects.

Macro Syntax (Controllers & LiveViews)

Use the param :field, :list do...end syntax for clean, declarative list schemas:

defmodule MyAppWeb.OrderController do
  use Filtr.Controller

  # List of items with nested validation
  param :items, :list do
    param :name, :string, required: true
    param :quantity, :integer, min: 1, default: 1
    param :price, :float, min: 0
  end

  def create(conn, params) do
    # params.items is a list of validated item objects
    # Each item has validated name, quantity, and price fields
    total = Enum.reduce(params.items, 0, fn item, acc ->
      acc + (item.quantity * item.price)
    end)

    json(conn, %{total: total})
  end
end

LiveView with URL Parameters:

defmodule MyAppWeb.UserLive do
  use Filtr.LiveView

  param :users, :list do
    param :name, :string, required: true
    param :age, :integer, min: 18
  end

  def mount(_params, _session, socket) do
    # socket.assigns.filtr.users is a validated list
    {:ok, socket}
  end
end

# URL format: /users?users[0][name]=John&users[0][age]=25&users[1][name]=Jane&users[1][age]=30
# Converts to: [%{name: "John", age: 25}, %{name: "Jane", age: 30}]

Map Syntax (Standalone)

For standalone usage, use the map syntax with {:list, schema}:

schema = %{
  items: [
    type: {
      :list,
      %{
        name: [type: :string, validators: [required: true]],
        quantity: [type: :integer, validators: [min: 1]]
      }
    }
  ]
}

params = %{
  "items" => [
    %{"name" => "Product A", "quantity" => "5"},
    %{"name" => "Product B", "quantity" => "3"}
  ]
}

result = Filtr.run(schema, param

Related Skills

View on GitHub
GitHub Stars14
CategoryDevelopment
Updated6d ago
Forks0

Languages

Elixir

Security Score

75/100

Audited on Apr 3, 2026

No findings