SQL-like interface for querying APIs
What if you could query any API like you would a database? Introducing Apipe, a library that provides a SQL-like interface for querying APIs. It's Provider interface allows you to easily add support for new APIs.
- SQL-like query interface inspired by Supabase & Ecto
- Flexible filtering, joining and chainable operators
- Optional type-safe responses with casting
- Comprehensive error handling
- Extensible provider system
Add apipe
to your list of dependencies in mix.exs
:
def deps do
[
{:apipe, "~> 0.1.0"}
]
end
import Apipe
alias Apipe.Providers.GitHub
# Create a new GitHub client (auth token and cast_response are optional)
github = Apipe.new(GitHub)
github
|> from("repos/cpursley/apipe")
|> execute()
# Using map-based where filters (with casting)
github
|> from("search/repositories")
|> where(%{language: "elixir", name: [like: "phoenix"], stars: [gt: 1000, lte: 10000]})
|> order_by(:stars, :desc)
|> limit(3)
|> execute()
# Query using chainable operators with select (without type casting)
Apipe.new(GitHub, cast_response: false)
|> select([:id, :name, :stargazers_count])
|> from("search/repositories")
|> eq(:language, "elixir")
|> gt(:stars, 1000)
|> lte(:stars, 10000)
|> order_by(:stars, :desc)
|> limit(3)
|> execute()
# Combining both styles (with casting)
github # using the client with casting enabled (we set up earlier)
|> select(:name) # only name field will be cast to repository struct
|> from("search/repositories")
|> where(:language, eq: "elixir") # using field-operator syntax instead of maps
|> gt(:stars, 1000)
|> lte(:stars, 10000)
|> like(:name, "phoenix")
|> order_by(:stars, :desc)
|> limit(3)
|> execute()
# Joining related resources
contributors = fn repo -> "repos/#{repo["full_name"]}/contributors" end
github
|> from("search/repositories")
|> eq(:language, "elixir")
|> order_by(:updated) # Find repositories with recent activity
|> limit(3)
|> join(:contributors, fn repo -> # join top contributors
github
|> from(contributors.(repo))
|> limit(2)
end)
|> execute()
Apipe supports a variety of filter operators that can be used in both map-based where
clauses and as chainable functions:
- Equality:
where(%{field: value})
oreq(field, value)
- Not Equal:
where(%{field: [neq: value]})
orneq(field, value)
- Greater Than:
where(%{field: [gt: value]})
orgt(field, value)
- Greater Than or Equal:
where(%{field: [gte: value]})
orgte(field, value)
- Less Than:
where(%{field: [lt: value]})
orlt(field, value)
- Less Than or Equal:
where(%{field: [lte: value]})
orlte(field, value)
- IN:
where(%{field: [in: values]})
orin_list(field, values)
- NOT IN:
where(%{field: [nin: values]})
ornin_list(field, values)
- LIKE:
where(%{field: [like: pattern]})
orlike(field, pattern)
- Case-insensitive LIKE:
where(%{field: [ilike: pattern]})
orilike(field, pattern)
You can combine multiple operators for the same field in a where clause:
where(%{stars: [gt: 100, lte: 1000]})
All responses are wrapped in a Response
struct that includes:
data
: The actual response data (typed if usingcast_response: true
, raw JSON otherwise)- Additional metadata specific to the provider (rate limits, pagination info, etc.)
Type casting is configured when creating a new client with the cast_response: true
option. When enabled, responses will be converted into Elixir structs with proper types. Without it, you'll get raw JSON responses. Choose based on your needs:
-
Enable casting when you want:
- Type safety
- Better IDE support
- Cleaner access to nested data
- Example:
repo.full_name
,repo.stargazers_count
-
Skip casting when you want:
- Raw JSON responses
- Better performance
- To access fields not defined in the type structs
- Example:
repo["full_name"]
,repo["stargazers_count"]
Apipe uses a provider-based architecture to support multiple API integrations. A provider is a module that implements the Apipe.Provider
behaviour and handles the specifics of communicating with a particular API.
Each provider is responsible for:
- Route matching and validation
- Schema resolution and type casting
- Request building and execution
- Response processing and transformation
Apipe includes built-in support for generating providers from OpenAPI specifications. This process involves:
- Converting the OpenAPI spec to Elixir types using
oapi_generator
:
# In config/config.exs
config :oapi_generator,
github: [
output: [
base_module: GitHubOpenAPI,
extra_fields: [
__info__: :map,
# required for joins
__joins__: :map
],
location: "lib/providers/github/openapi",
operation_subdirectory: "operations/",
schema_subdirectory: "schemas/",
schema_use: Apipe.Providers.OpenAPI.Encoder
]
]
- Generating the provider's route module using the mix task:
# Local file
mix apipe.gen.openapi.provider path/to/spec.yaml provider_name [--module-name NAME]
# Remote file
mix apipe.gen.openapi.provider --url https://example.com/openapi.yaml provider_name [--module-name NAME]
This generates:
- Route matching logic for API endpoints
- Schema resolution for response types
- Validation for paths and parameters
To create a new provider:
- Add the OpenAPI specification to your project
- Configure
oapi_generator
for your provider - Generate the types and routes:
# Generate types from OpenAPI spec
mix oapi.gen path/to/openapi-spec.yaml
# Generate routes
mix apipe.gen.openapi.provider path/to/openapi-spec.yaml provider_name
- Implement the provider module:
defmodule Apipe.Providers.MyProvider do
@behaviour Apipe.Provider
alias Apipe.Providers.MyProvider.Routes
# Implement the required callbacks
def execute(query, opts \\ []) do
# Your implementation
end
end
The provider can then be used with Apipe's query interface:
Apipe.new(Apipe.Providers.MyProvider)
|> Apipe.from("some/endpoint")
|> Apipe.execute()
Contributions are welcome! Feel free to open issues or submit pull requests.
This project is licensed under the MIT License.