An Elixir library for defining and verifying API response contracts.
- DSL for contract definition: Define API contracts using an intuitive macro-based DSL
- Pure validation logic: All validation is side-effect free and testable
- Error accumulation: Collects all validation errors at once, not just the first one
- Type checking: Supports integer, string, boolean, float, array, and object types
- Optional fields: Define required and optional response fields
- Array validation: Validate arrays with element type checking
- No external dependencies: No Phoenix, no HTTP clients, just pure Elixir logic
Add api_verifier to your mix.exs:
def deps do
[
{:api_verifier, path: "."} # or from hex when published
]
end
Then run:
mix deps.get
Create a module that uses the ApiVerifier DSL:
defmodule MyAPI.Contracts do
use ApiVerifier.DSL
contract "GetUser" do
response 200 do
field :id, :integer
field :name, :string
field :email, :string
field :active, :boolean
field :tags, :array, array_type: :string
field :metadata, :object, required: false
end
response 404 do
field :error, :string
field :message, :string
end
end
end
contract = ApiVerifier.DSL.get_contract(MyAPI.Contracts, "GetUser")
response_data = %{
id: 123,
name: "Alice",
email: "alice@example.com",
active: true,
tags: ["admin", "user"]
}
case ApiVerifier.validate(contract, 200, response_data) do
{:ok, data} ->
IO.puts("Response is valid!")
IO.inspect(data)
{:error, errors} ->
IO.puts("Response validation failed:")
Enum.each(errors, fn error ->
IO.inspect(error)
end)
end
:string- Expects a binary value:integer- Expects an integer:float- Expects a float or integer:boolean- Expects true or false:array- Expects a list (optionally with element type checking viaarray_type:option):object- Expects a map
Mark a field as optional:
field :description, :string, required: false
Specify the type of elements in an array:
field :tags, :array, array_type: :string
field :ids, :array, array_type: :integer
The validator returns structured errors with the following error types:
:missing_field- A required field is missing from the response:type_mismatch- A field has the wrong type:unexpected_field- The response contains a field not in the contract:no_response_spec- No specification exists for the given status code
Each error is a ApiVerifier.Validator.ValidationError struct with:
field- The field name (or nil for status/spec-level errors)error- The error type (atom)expected- What was expectedactual- What was actually received
defmodule ProductAPI do
use ApiVerifier.DSL
contract "GetProduct" do
response 200 do
field :id, :integer
field :name, :string
field :price, :float
field :in_stock, :boolean
end
end
end
contract = ApiVerifier.DSL.get_contract(ProductAPI, "GetProduct")
response = %{id: 1, name: "Widget", price: 9.99, in_stock: true}
{:ok, _} = ApiVerifier.validate(contract, 200, response)
defmodule UserAPI do
use ApiVerifier.DSL
contract "CreateUser" do
response 201 do
field :id, :integer
field :name, :string
field :email, :string
field :bio, :string, required: false
end
end
end
defmodule BlogAPI do
use ApiVerifier.DSL
contract "ListPosts" do
response 200 do
field :posts, :array, array_type: :object
field :total, :integer
end
end
end
defmodule PaymentAPI do
use ApiVerifier.DSL
contract "ProcessPayment" do
response 200 do
field :transaction_id, :string
field :amount, :float
field :status, :string
end
response 400 do
field :error, :string
field :code, :string
end
response 402 do
field :error, :string
field :message, :string
end
end
end
# Get contract
contract = ApiVerifier.DSL.get_contract(PaymentAPI, "ProcessPayment")
# Validate different responses
ApiVerifier.validate(contract, 200, success_response)
ApiVerifier.validate(contract, 400, error_response)
ApiVerifier.validate(contract, 402, payment_required_response)
ApiVerifier is built with a focus on:
- Pure functions: All validation logic is pure and testable
- Compile-time DSL: Contracts are compiled at compile time into immutable data structures
- Separation of concerns: Macros handle the DSL, pure functions handle validation
- No side effects in core: All side effects live at the edges (CLI, HTTP handlers, etc.)
- Structured errors: All validation errors are structured data, never exceptions
lib/
api_verifier.ex # Main public API
api_verifier/
contract.ex # Data structures for Contract, ResponseSpec, FieldSpec
validator.ex # Pure validation logic
dsl.ex # Macro-based DSL for defining contracts
test/
api_verifier_test.exs # Integration tests
test_helper.exs # Test setup
api_verifier/
contract_test.exs # Contract data structure tests
validator_test.exs # Validator logic tests
dsl_test.exs # DSL macro tests
Run the test suite:
mix test
Run with verbose output:
mix test --trace
Run a specific test file:
mix test test/api_verifier/validator_test.exs
Contracts are defined at compile time using macros and compiled into immutable data structures. No runtime DSL evaluation.
The Validator.validate/3 function is pure:
- No side effects
- Deterministic
- Fully testable
- Easy to compose
The validator doesn't stop at the first error. It accumulates all errors and reports them together, making it easier to see all problems at once.
The library uses Elixir's type system to define contracts and validate against them. All inputs and outputs are clearly typed.
The architecture makes it easy to extend with new features:
- New field types can be added by extending the validator
- New error types can be added to the error structure
- The pure validation logic can be composed with other functions
While the current milestone focuses on basic functionality, future versions could add:
- Nested object validation (detailed validation of nested structures)
- String pattern validation (regex, format)
- Number range validation
- Custom validation functions
- Response header validation
- HTTP status-specific error responses
- Integration with HTTP clients
- CLI tool for validating real API responses
- Swagger/OpenAPI integration
MIT