Elixir is a modern functional programming language that thrives on performance, scalability, and maintainability. When building APIs in Elixir, most developers typically opt for a framework like Phoenix for its out-of-the-box capabilities and conventions. However, you can also create an API with just Plug, allowing for a leaner, more minimalistic approach. Plug is a powerful library that enables direct interaction with HTTP requests and responses, making it an excellent tool for those who want full control over their application’s behavior.
What is Plug?
Plug is a specification for composing modules and handling HTTP requests in Elixir. It provides a simple and flexible API for building web applications or services. At its core, Plug defines two key concepts:
Plug.Conn
: A data structure that represents the connection and holds request/response information.Plug Modules
: Components that implement the plug behavior to transform or process requests.
Setting Up Your Project
To start, you’ll need to create a new Mix project. Run the following command:
mix new my_api --sup
Next, add plug_cowboy
to your dependencies in mix.exs
:
defp deps do
[
{:plug_cowboy, "~> 2.0"}
]
end
Then, fetch the dependencies:
mix deps.get
Simple example
Writing Your First Plug
Plug allows you to create composable modules for handling various aspects of your API. Begin by creating a new module for your plug:
defmodule MyAPI.HelloPlug do
import Plug.Conn
def init(options), do: options
def call(conn, _opts) do
conn
|> put_resp_content_type("application/json")
|> send_resp(200, ~s({"message": "Hello, world!"}))
end
end
This module implements two functions, init/1
and call/2
, as required by the
Plug behavior. The call/2
function takes a Plug.Conn
and processes it before
returning the modified connection.
Setting Up the Router
To route incoming requests to your plug, you can use the Plug.Router
module:
defmodule MyAPI.Router do
use Plug.Router
plug :match
plug :dispatch
get "/hello" do
send_resp(conn, 200, "Welcome to the Elixir API!")
end
match _ do
send_resp(conn, 404, "Not Found")
end
end
Starting the Application
To serve your API, you need to use a web server. Plug integrates seamlessly with Cowboy, a lightweight HTTP server. Update your application’s supervisor to start the router:
defmodule MyAPI.Application do
use Application
def start(_type, _args) do
children = [
{Plug.Cowboy, scheme: :http, plug: MyAPI.Router, options: [port: 4000]}
]
opts = [strategy: :one_for_one, name: MyAPI.Supervisor]
Supervisor.start_link(children, opts)
end
end
Now, run your application:
iex -S mix
Visit http://localhost:4000/hello
to see your API in action!
Advanced example
If you’re ready to go beyond the basics, let’s explore how to build a more complex API endpoint that handles JSON parsing in requests and sends responses in JSON format. We’re going to expand on our earlier example and create an endpoint that accepts a JSON payload, processes the data, and sends back a meaningful JSON response. Let’s break it down step by step.
Extending the Setup
Ensure your dependencies include plug_cowboy
(for the HTTP server) and
jason
, a JSON library in Elixir, for parsing and encoding JSON. Update the
deps
section in mix.exs
:
defp deps do
[
{:plug_cowboy, "~> 2.0"},
{:jason, "~> 1.4"}
]
end
After adding these, fetch the dependencies with:
mix deps.get
Writing an Advanced Plug
Now, let’s create a Plug that accepts JSON data in the request body, processes
it, and responds with JSON. For this, we will import functions from Plug.Conn
and use Jason
for JSON handling.
Create a new Plug module:
defmodule MyAPI.UserPlug do
import Plug.Conn
def init(options), do: options
def call(conn, _opts) do
# Ensure we have a valid JSON content-type in the request
case get_req_header(conn, "content-type") do
["application/json"] ->
# Read and parse the JSON payload
{:ok, body, _conn} = Plug.Conn.read_body(conn)
case Jason.decode(body) do
{:ok, %{"name" => name, "age" => age}} ->
# Process the JSON data and construct a response
response = %{
message: "User data received",
data: %{
name: name,
age: age,
status: if age >= 18, do: "Adult", else: "Minor"
}
}
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(response))
{:error, _reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(400, Jason.encode!(%{error: "Invalid JSON format"}))
end
_ ->
# Handle missing or incorrect content-type
conn
|> put_resp_content_type("application/json")
|> send_resp(415, Jason.encode!(%{error: "Unsupported Media Type"}))
end
end
end
Code Breakdown
Header Validation:
- Checks if the request has a
Content-Type
ofapplication/json
. If not, it sends a415 Unsupported Media Type
response.
JSON Parsing:
- Reads the request body using
Plug.Conn.read_body/1
. - Decodes the body into a map using
Jason.decode/1
. If decoding fails, sends a400 Bad Request
response with an error message.
Response Construction:
- Extracts
name
andage
from the request JSON. - Constructs a JSON response with a custom message and additional data, such as
the user’s
status
(Adult or Minor).
Output:
- Sends the response with a
200 OK
status, along with the JSON-encoded data.
Updating the Router
To use this new plug, let’s update the router to add an endpoint for handling
POST requests. We’ll also include Plug.Parsers
to handle JSON requests
gracefully:
defmodule MyAPI.Router do
use Plug.Router
plug Plug.Parsers,
parsers: [:json],
pass: ["application/json"],
json_decoder: Jason
plug :match
plug :dispatch
post "/users" do
MyAPI.UserPlug.call(conn, [])
end
match _ do
send_resp(conn, 404, "Not Found")
end
end
What’s Happening Here?
Plug.Parsers:
- This plug pre-processes incoming requests, allowing us to easily parse JSON payloads in the body.
Dynamic Plug Handling:
- The
/users
endpoint delegates the logic to ourMyAPI.UserPlug
.
Fallback Match:
- A
404 Not Found
response is sent if no known route matches the incoming request.
Running and Testing the API
Start your application:
iex -S mix
Now you can test your API by sending a POST request with a JSON payload using a
tool like curl
, Postman, or even an HTTP client library.
Example Request using curl
:
curl -X POST http://localhost:4000/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "age": 30}'
Example Response (status 200):
{
"message": "User data received",
"data": {
"name": "Alice",
"age": 30,
"status": "Adult"
}
}
If you send an invalid JSON payload, the API will reply with an appropriate error message (status 400). If the content type is missing or incorrect, it will return status 415.
Why This Matters
With this approach, you’re now able to handle robust, dynamic JSON data
exchanges in your API endpoints. By leveraging Plug.Conn
and libraries like
Jason
, you can create custom logic while still keeping your Elixir application
lightweight and nimble.
Why Use Plug Only?
Building an API with just Plug gives you granular control over each request and response. Without the overhead of a full framework, you can achieve better performance and a smaller codebase, ideal for lightweight services or learning the internals of web development in Elixir.
Closing Thoughts
Using Plug for an Elixir API not only teaches you a lot about HTTP handling but also gives you the flexibility to build exactly what you need without the additional abstractions of larger frameworks. It’s perfect for simple APIs or small tools where minimalism is key. Experiment with Plug, and you’ll uncover new ways to harness the elegance and power of Elixir!
Share on
Twitter Facebook LinkedInHave comments or want to discuss this topic?
Send an email to ~bounga/public-inbox@lists.sr.ht