Build a simple, production-ready REST API with MongoDB in Elixir without Phoenix.
In the previous post, we covered how to define compile-time configuration, set up MongoDB locally using Docker, and configure the Mongo client to talk to the database. If you reached this post directly, you might want to read the last post, as this is a continuation of that post.
In this part of the course we will cover:
- Handling JSON data
- Setting up CRUD endpoint for a post.
Let's get started
For the sake of simplicity, our application will expose APIs with which we will be able to perform the following actions:
- Create a Post
- View the Post
- Update the Post
- Delete the Post
- List all the Posts
The first order of business will be to design these endpoints using Postman. I have created a public workspace that contains the specification of the API in form of a collection.
Now that we have a clear idea about what our interface looks like, we start the implementation and add code to our application.
The content type of data that we send across in our REST API calls is consistently JSON (Javascript Object Notation)[1].
Trivia - JSON has been derived from Javascript language which is the defacto standard for scripting on the web for over two decades now. Although JSON is supposed to be language-agnostic it is not a first-class citizen in most languages, including Elixir. In elixir, we use the Jason library to be able to serialise and deserialize the JSON string.
The default Jayson encoder implementation is very simple and encodes the different data structures with a predefined algorithm. This behaviour is acceptable for the majority of the use cases, but there are certain cases where we might want to customize the serialisation algorithm.
An example of such a case where encoding customisation is required is during the encoding of MongoId.
Encoding MongoDB ObjectIds
MongoDB stores data in its database in the form of BSON (Binary JSON) which is a custom format designed by the MongoDB team back in 2009. Like all databases, the data in the database need to be indexed using a primary key.
The primary key of every collection in MongoDB is a special binary value called the ObjectID. It is so special that Jayson has no clue how to encode the ObjectId to JSON. So let's do that first.
💡Concept - Protocols
A few questions that might be coming to your curious mind could be
- What just happened here?
- What does
defimp
mean? - How are we able to change encoding behaviour with this?
The answer to all the questions is a special mechanism provided by Elixir called Protocols.
Protocols are a mechanism to achieve polymorphism in Elixir when you want behavior to vary depending on the data type.
If you are familiar with Object-Oriented language, then such behaviour is usually implemented using Interfaces
. Different data types defined a method
enforced by an Interface to handle the polymorphism.
Let's take an example in typescript and contrast it with Elixir.
// Lets consider we have two vehicles a Car and Train for which we want to ensure we can locate them.
interface Locability {
location: () => [longitude: number, latitude: number]
}
class Car {
location () {/* Using the GPS */}
}
class Train {
location () {/* From transponder or something */}
}
The same implementation in Elixir would look something like this.
# Equivalent to an interface
defprotocol Locability do
@spec type() :: {integer, integer}
def location()
end
defimpl Locability, for: Car do
def location(), do: # Using the GPS
end
defimpl Locability, for: Train do
def location(), do: # For transponder or something
end
We observe that they are semantically the same but syntactically different.
Converting _id to id
Another small but important aspect that we need to handle is converting the _id
key in a document returned by MongoDB to id
. In MongoDB, the primary key is stored as _id
but we want the id to be id
when we send the response in JSON.
For the sake of brevity, let's alias RestApi.JSONUtils
to just JSON
so that it's for our eyes to read during it's usage.
Endpoints
Get all Posts
The HTTP endpoint to get all posts will be GET /posts
Create a Post
The HTTP endpoint for creating a post will be POST /posts
Get a Post by Id
The HTTP endpoint for getting a post by id will be GET /post/:id
Update a Post by Id
The HTTP endpoint for updating a post by id will be PUT /post/:id
Delete a Post by Id
The HTTP endpoint for deleting a post by id will be DELETE /post/:id
Run the server
I know we have not written any tests yet, but let's first manually test our application. To start running the application execute the following command on your terminal $iex -S mix run
. If your application is already running, then just enter the command recompile
.
The next step is to again use Postman to test out the endpoints with some sample data to see how it works. The collection already has a few examples that you can use to test out the endpoints.
Next
This will be the end of part 3. To sum up, what we have learned so far,
- You know how to configure Jason to work with MongoDB.
- You know how to create REST endpoints with Plug.
- You know how to query MongoDB to perform CRUD operations.
- You know how to manually test your endpoints using Postman collections.
In the next post, we will add tests for our endpoints and understand what we can do to protect our endpoints from unauthorized access.
If you liked this tutorial then please support me by sharing this post with your friends and, if you like, consider becoming a member (it's free). 🎉
References
- JSON - RFC 7159 - https://datatracker.ietf.org/doc/html/rfc7159
- Protocol - https://hexdocs.pm/elixir/1.12.3/Protocol.html
- MongoDB Functions - https://hexdocs.pm/mongodb_driver/Mongo.html#functions
Disclaimer: The ideas and opinions in this post are my own and not the views of my employer.