Build a simple, production-ready REST API with MongoDB in Elixir without Phoenix.
In the previous post, we covered the basics of building a simple HTTP server. If you reached this post directly, you might want to read the last post, as this is a continuation of that post.
In the part of the course we will cover:
- How to use the Config module to define configurations for different environments
- Setup MongoDB locally and use a MongoDB client library to connect to it.
- Update HTTP server to expose a health-check endpoint
Let's get started
We often do not want to start the web server on the port 80
when developing locally as it is a privileged port. Ideally, we want to run the application locally in a port >1024
and when we are deploying to production, use port 80
. We might also want to run our tests in a different port as we don't want to keep shutting down our dev-server whenever we want to test our application.
This all comes down to is that we need different configurations for different environments we want to run our application. We start with just the port, but later we find more use-case like the database configuration, etc.
Adding Config
Elixir provides us with two configuration entry points that it understands.
config/config.exs
- This file is read at the build time, i.e. before we compile our application and load our dependencies. Protip: We can use this to control how our application is compiled.config/runtime.exs
- This file is read after compilation and configures how the application works at runtime. We can read system environment variables (System.get_env/1
) or any external configuration in this file.
Let's add the port configuration to the compile-time config. (It is very tempting to add it as a runtime config, but come on, how many times have you needed to change it?)
So what is happening in this file? We are importing the Config module and then using the import_config
function to import an environment-specific configuration file. The config_env
function returns the environment this file is being executed on. By default, mix starts with dev
environment and test
when running tests. The environment can be overridden using the MIX_ENV
environment variable or performing a mix release
.
So to cover all of our bases, we will create one config file for each of the dev
, test
and prod
environments with the same signature.
The config function has two variants config/2
and config/3
. We are using the config/3
variant and app atom :rest_api
as the root_key
for defining the port key-value pair.
Now let's update the application.ex
file to use the config instead of using the hardcoded port 8080
.
If you have the mix application already running, then just run iex(1)> recompile
or else run the application with iex -S mix run
. Head over to Postman to check if the endpoint is still responding 🤞. (If you have followed all sets corrects, then it should all be working fine)
Setup MongoDB
No production service is complete without a database to store our data. There are a lot of different kinds of databases like SQL based Relational databases, No-SQL document databases, Timeseries databases, etc.
Over the last few years, MongoDB has become the most popular document-based No-SQL database, easy to set up and scale to production loads. Cloud providers like Amazon AWS also provide managed services like DocumentDB, which expose the same API as MongoDB.
We can always install MongoDB natively for our specific platform, but one of the simplest reproducible ways to set up a dev environment is by using Docker and Docker Compose.
Docker provides an easy-to-use executable to start running Docker on Windows, macOS or Linux.
Once Docker is installed, we want to create the docker-compose.yml
configuration file in the root of the application.
version: "3"
services:
mongodb:
image: mongo
container_name: mongodb
ports:
- 27017:27017
restart: unless-stopped
healthcheck:
test: test $$(echo "rs.initiate({_id:\"rs0\",members:[{_id:0,host:\"localhost:27017\"}]}).ok || rs.status().ok" | mongo --port 27017 --quiet) -eq 1
interval: 10s
start_period: 30s
command: "mongod --bind_ip_all --replSet rs0"
What we are doing here is telling MongoDB daemon to start with a replica-set rs0
and then using the health check feature provided by docker-compose to wait until all replicas are up and running.
Let's start MongoDB using Docker. Run the following command in your terminal. We are using the -d
flag to run the container as a daemon to re-use the terminal.
You should see an output similar to this:
[+] Running 2/2
⠿ Network rest_api_default Created 0.0s
⠿ Container mongodb Started 0.6s
Once you are done with local development, don't forget to bring down the service using docker compose down
.
While you are at it, go ahead and install Mongo Compass so that you can use the GUI client to look at your database.
The default installation of MongoDB does not have any or username defined password, so you can connect to it using this connection string. mongodb://localhost:27017
.
Next, go ahead and create a database rest_api_db
and a collection users
.
Connect to MongoDB
The MongoDB database is configured and running on our local system now. The only thing left is getting a MongoDB client library for elixir and connecting to the database.
We have to choices for this MongoDB and mongodb_driver. It seems that mongodb_driver is more recent appears to be actively developed; therefore, we will use it.
Let's update the mix.exs
file to add it as a dependency. We will also install another dependency called poolboy
to manage the connection pool.
Again, go ahead and do a mix deps.get
to get the dependencies installed.
Next, we need to update lib/application.ex
again to add Mongo to the supervisor.
As you can also see that we have defined two more configurations :database
and :pool_size
we should add it to the respective config file. For now, let's update all of the env files with the same database name and pool size.
Go ahead and recompile
the application. You will not be able to see any difference yet. Let's now write a simple health check endpoint that will ping the database and tell us if we are connected to the database.
Health check Endpoint
The health check endpoint is very simple. We send a ping command to MongoDB and respond with 200 OK
status code on success. In this way, we will know if ever our database goes down and we need to halt operations.
Let's go through this line-by-line. First, we add a new endpoint GET /knockknock
that will act as our health check endpoint. If everything is ok, the endpoint should return 200 OK
with the message `Who's there?`.
Next, use a case
statement to pattern match the result of Mongo.command
function to respond 200 if success or respond 500 in case of an error.
The first parameter of Mongo.command
is the name of our MongoDB GenServer, which we had defined to be the atom :mongo
.
Now go ahead and recompile
your application again and test out using Postman if the health-check endpoint is working.
Next
Now, this is the end of part 2. To sum up, what we have learned so far, you now know how to define compile-time configuration for your applications, you know how to set up MongoDB locally using Docker, and you can use the Mongo client to talk to the database.
In the next post, we will set up the CRUD endpoints for a resource.
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). 🎉
Disclaimer: The ideas and opinions in this post are my own and not the views of my employer.