Let's get a Liberator endpoint up and running as fast as possible.
First, you'll probably want to fire up a Phoenix application.
Check out Phoenix's Installation Guide
and their Up and Running Guide
and install it if you don't have it already,
then create a new project with phx.new
.
Since Liberator was built to help create APIs, we will disable HTML generation. We'll keep Ecto, though.
$ mix phx.new hello --no-html --no-webpack
We'll skip the details of setting up an Ecto repo, because it's not necessary to have one running to use Liberator. However, your data probably lives in a repo, so this example will contain some references to one.
Add liberator
to your list of dependencies in mix.exs
def deps do
[
{:liberator, "~> 1.3.0"}
]
end
Then we'll use mix phx.gen.json
to create a basic schema, route, and context.
It will create a controller as well, but we'll be replacing that with a Liberator Resource.
$ mix phx.gen.json Blog Post posts title:string content:string
Migrate the database as the command feedback requests, if you've got your database handy.
However, don't copy the statement they're asking you to put in lib/hello_web/router.ex
.
We're going to use our own.
In lib/hello_web/router.ex
, forward any /posts
requests to the resource we're about to define.
defmodule HelloWeb.Router do
use HelloWeb, :router
forward "/posts", HelloWeb.PostsResource
end
Now we must actually create our first Liberator resource.
Go ahead and delete the file at lib/controllers/posts_controller.ex
that Phoenix generated for you,
and create a file at lib/controllers/posts_resource.ex
with the following content.
defmodule HelloWeb.PostsResource do
use Liberator.Resource
end
Run the project with mix phx.server
and check out http://localhost:4000/posts
.
With Liberator's sensible defaults, it should return a status of 200 and content of OK
.
$ curl --location --request GET 'http://localhost:4000/posts/'
HTTP/1.1 200 OK
allow: GET, HEAD
cache-control: max-age=0, private, must-revalidate
content-encoding: identity
content-length: 2
content-type: text/plain
date: Thu, 15 Oct 2020 22:54:57 GMT
server: Cowboy
x-request-id: Fj5MYV-9JH1y19wAACQl
OK
Let's spice that up a little more and get some dynamic content in here.
defmodule HelloWeb.PostsResource do
use Liberator.Resource
@impl true
def handle_ok(_conn) do
DateTime.utc_now() |> DateTime.to_iso8601()
end
end
$ curl --location --request GET 'http://localhost:4000/posts/'
HTTP/1.1 200 OK
allow: GET, HEAD
cache-control: max-age=0, private, must-revalidate
content-encoding: identity
content-length: 27
content-type: text/plain
date: Thu, 15 Oct 2020 22:53:42 GMT
server: Cowboy
x-request-id: Fj5MT91NPreSB7QAABqF
2020-10-15T22:53:30.668000Z
That's barely better than regular Phoenix controllers, so let's go further. Since we're trying to write an API, why don't we grab our Repo and try to get some data out in the world?
defmodule HelloWeb.PostsResource do
use Liberator.Resource
@impl true
def handle_ok(conn) do
id = List.last(conn.path_info)
post = Hello.Blog.get_post!(id)
post.title <> "\n\n" <> post.content
end
end
Now if you try to hit http://localhost:4000/posts/5
, we get a Phoenix exception.
Nothing is in the database, of course.
We must check to see if the given ID exists in the database,
and here's where Liberator's features start to show.
Implement the c:Liberator.Resource.exists?/1
callback in your resource.
Here we can check for the resource.
In fact, let's just grab the entire thing and stick in back in the conn
.
The exists?/1
callback expects you to return a boolean, a Map
, or the conn
.
If you return a Map
, Liberator will merge that into conn.assigns
.
defmodule HelloWeb.PostsResource do
use Liberator.Resource
@impl true
def exists?(conn) do
id = List.last(conn.path_info)
try do
Hello.Blog.get_post!(id)
rescue
Ecto.NoResultsError -> false
ArgumentError -> false
else
post -> %{post: post}
end
end
@impl true
def handle_ok(conn) do
post = conn.assigns[:post]
post.title <> " " <> post.content
end
end
$ curl --location --request GET 'http://localhost:4000/posts/1555'
HTTP/1.1 404 Not Found
allow: GET, HEAD
cache-control: max-age=0, private, must-revalidate
content-encoding: identity
content-length: 9
content-type: text/plain
date: Thu, 15 Oct 2020 22:52:25 GMT
server: Cowboy
x-request-id: Fj5MPb9fgMWho78AABkF
Not Found
Now we get a nice "Not Found" response! So, last, we want to be able to POST something. That introduces us to the concept of actions, which are just another kind of overridable function. There are four actions in Liberator:
delete!
patch!
post!
put!
As you could guess, these actions will be called based on the request's HTTP method.
For now, we'll set up post!
.
Add c:Liberator.Resource.post!/1
to insert the params.
We also want to make sure that we allow POST requests,
so define c:Liberator.Resource.allowed_methods/1
too.
We could parse the request body here if we wanted, but Plug provides the Plug.Parsers
plug,
and it's already set up in this beginner project for accepting JSON.
The params are available in conn.params
, so we can pass those into our context module directly,
and let the changeset handle validation.
defmodule HelloWeb.PostsResource do
use Liberator.Resource
@impl true
def allowed_methods(_), do: ["POST", "GET"]
@impl true
def exists?(conn) do
id = List.last(conn.path_info)
case Hello.Repo.get(Hello.Blog.Post, id) do
nil -> false
post -> %{post: post}
end
end
@impl true
def post!(conn) do
{:ok, _post} = Hello.Blog.create_post(conn.params)
end
@impl true
def handle_ok(conn) do
post = conn.assigns[:post]
post.title <> " " <> post.content
end
end
That's all! Let's POST to our new endpoint now.
$ curl --location --request POST 'http://localhost:4000/posts' \
> --header 'Content-Type: application/json' \
> --data-raw '{
> "title": "My first post!",
> "content": "This is so fun!"
> }'
HTTP/1.1 201 Created
allow: POST, GET
cache-control: max-age=0, private, must-revalidate
content-encoding: identity
content-length: 7
content-type: text/plain
date: Thu, 15 Oct 2020 22:42:07 GMT
server: Cowboy
x-request-id: Fj5L-UYbiai2oD4AADaB
Created
Finally, point your browser at http://localhost:4000/posts/1.
curl --location --request GET 'http://localhost:4000/posts/1'
HTTP/1.1 200 OK
allow: POST, GET
cache-control: max-age=0, private, must-revalidate
content-encoding: identity
content-length: 30
content-type: text/plain
date: Thu, 15 Oct 2020 22:47:30 GMT
server: Cowboy
x-request-id: Fj5LreF6rvXe6igAABoD
My first post! This is so fun!
You've done it!
You've just built JSON endpoint with a fair amount of built-in smarts.
There's so much more to customize, but now you've got a base to work from.
Check out the documentation for Liberator.Resource
to see all the stuff you can do!