A Fruit Flavoured Guide to Creating a REST API powered by Node and MongoDB.
In this article we’ll go through the steps needed to have a working API that can fetch, insert and remove data from MongoDB, a non-relational database.
Personally I love eating fruit 🍎 so for this example we will be collecting peoples’ favorite fruit!
Once our API is up and running, ready to collect fruit, we’ll also explore ways to improve it.
If you just want to get straight to the code, click here.
Some cool things we will encounter on the way.
- How to create a simple express server locally.
- Understand the importance of testing.
- Learn how to insert, find and remove data from mongoDB using mongoose.
- Build endpoints that we can call from any client application.
Some things you should know first.
- This isn’t a node tutorial, so if you need to brush up on nodejs beforehand you can check out the docs here.
- You should be able to install packages using npm. If not you can check out npm here.
- You will need mongoDB set up locally. Depending on your OS, you can take a look at the osx, windows or linux guides.
So, whats the plan?
We want to build an express server, with 3 routes that will:
- Insert a new person’s favourite fruit into our database.
- Query peoples’ favourite fruit from our database.
- Remove a person’s favourite fruit from our database.
All of these requests can use the same endpoint /fruit
with ‘insert’ using the POST
method, ‘query’ using a GET
and ‘remove’ using a DELETE
.
Now we have laid out the basic foundation of what we want, we can think about a game-plan to achieve it.
One proposed option would be:
- Construct tests in order set up the criteria for what our API should do, using our expectations to drive our design.
- Having our tests set up we can begin construction 👷. Our objective will be to meet the requirements we planned out.
- If our design has been given the green lights, we can clean up any mess we made.
Setup
Assuming we have nodejs, npm and mongoDB installed, let’s initialize our project. Run the command npm init
in your terminal window, while in your folder directory and follow the instructions to create your projects package.json.
For an easier development process we’ll install nodemon, which will remove our need to repeatedly stop and start our server:
npm install -g nodemon
Now whenever we want to run our server, we will use the command:
$ nodemon index.js
With that in mind, we can set up our project structure to look something like this:
- The file routes.js is the place where we will build the functionality of our 3 routes
- The routes.test.js will be the file where we hold all of our routes’ tests.
- The config.js will hold any variables that need to be shared across different parts of the project.
- All our express server code and mongoose instantiation code can live in the index.js file.
- Our package.json will contain our dependencies and scripts.
Testing……or designing?
To make sure our endpoints work correctly and return the data we expect them to, we should construct tests to validate the responses we receive from our requests.
If the thought of writing tests doesn’t make you jump for joy and start cartwheeling down the street, I’m sure you’re not alone.
But a nicer way to think about “testing” is that you are actually laying down the design of your software.
Before any engineering task it’s always a good idea to detail out our product’s requirements.
For this guide, let’s keep things short and sweet by considering 2 requirements for each route:
Insert a new person’s favourite fruit into our database.
I would like to get a confirmation that my data has been inserted and I would also like to be notified of which data I have inserted.
So this route’s response:
- Should contain a
status
and thedata inserted
. - Should contain the
name
and thefavouriteFruit
that we inserted.
Query peoples’ favourite fruit from our database.
Our ideal response would contain the status and the fetched data, which could be multiple entries. So in this case, the route’s response:
- Should contain a
status
and thedata fetched
. - Should contain an array of fetched data, with each array item containing a
name
andfavouriteFruit
property.
Remove a person’s favourite fruit from our database.
Our ideal response would contain a status and a confirmation of the removed data. So our response:
- Should contain a
status
and thedata removed
. - Should contain the
name
and thefavouriteFruit
that we removed.
Building our tests.
For this tutorial I’ll be using jest. Which can be installed via the terminal by typing:
npm install --save-dev jest
Once we have jest installed as a dev dependency, we need to configure our test script, located in package.json by instructing our test
script to use jest:
"scripts": {
"test": "jest"
}
We will also need a way to send requests. I will be using axios for that:
npm i axios --save-dev
Looks like we are ready for action!. Using our established criteria above, here is what our routes.test.js file becomes:
Great! Ensure the server is running and then go ahead and type npm test
into the terminal. I wonder what delightful results we may get?
Well….. everything’s failing obviously. We don’t even have a server setup, let alone any routes. But what we do have is our requirements all set out, so we now know what to aim for. Lets go get it!
Build an express server
You can install express and save it as a project dependency by typing the following command in your project’s root folder:
$ npm i express --save
Once installed, let’s build our express server inside our index.js file. We can start by instantiating express and configuring it to listen on a port of our choosing.
Now that we have a server running, we can look to build out our routes:
We will need to import our routes into index.js so our express server can use them. This updates our index.js to:
Let’s test run our tests again and see how we are doing:
There you have it, a fully operational API…..
…….just kidding.
There’s an easy way to break this, what if we made another test and inserted a different name / favouriteFruit?
This example shows us that our testing suite isn’t a perfect fit for our requirements. Ideally we should come up with more conditions and write tests, but for the purpose of this guide, we will carry on.
Ok! Now it’s time to build our functionality out for real.
By this stage make sure you have your mongoDB server up and running.
You can install mongoose in similar fashion to express by typing in the terminal:
$ npm i mongoose --save
Once installed, we can import mongoose, configure our database and then instruct mongoose to connect to our mongoDB database:
Build a schema and model.
Earlier, we defined our ideal data structure through our tests. It looks like this:
{
"name": "Dan",
"favouriteFruit": "kiwi"
}
It’s great that we know it, but sadly mongoDB has absolutely no idea about our data structure. It’s time to let mongoDB know about our intentions.
We can do that by creating a schema and a model for our data.
A schema is simply just a guideline for how the data should be represented. So for our project, we want two keys: a "name"
and "favouriteFruit"
with their values both being strings. So to create our schema, all we need to do is specify that:
const FruitSchema = new mongoose.Schema({
name: String,
favouriteFruit: String
});
Now that we have our schema sorted, we can create a model. A model corresponds to a collection in mongoDB. So by adding our model it enables us to retrieve, add and store data for that specific model in our mongoDB:
const FavouriteFruit = mongoose.model("fruits", FruitSchema);
Alright! So our index.js file now looks like this:
So now we can move on to making our routes perform the actions we want them to!
Adding functionality to our routes.
Moving back to our routes.js file, currently all we are doing is responding with some dummy data. Time to change that.
Inserting data
Lets start with our POST
route because, well…it will be more fun to add some data before we try to view and delete data which does not exist yet 😲 .
So for the POST
request, we would expect that a user of the API will submit their name and favouriteFruit, like in our data structure above. We can therefore expect the incoming request to include a request body.
Fortunately we can access the incoming request body by accessing the req.body
argument inside our addToDatabase
function……………
……….if we add another npm package called body-parser.
As the name suggests, it will enable our express server to parse incoming request bodies.
$ npm i body-parser --save
Import and invoke it as a middleware, so our index.js grows by these lines:
Now, we need to build out the functionality that will enable us to store this information in our database. Mongoose has an inbuilt function called create
, which we can invoke to insert our data. Calling create
gives us a promise, which we can handle using .then .catch
.
Viewing Data
We can query our database by using mongoose’s method find
. Whats more is we can also pass in a query object, which will enable us to search for entries that contain the values we seek.
Our query object will be stored inside of req.query
, which we can subsequently pass into the find
method. Our pattern will remain similar to our post
example above:
Removing Data
The method we will use for removing data will be findOneAndDelete
, which takes an object as an argument, specifying which item to delete. We can therefore enable users to specify their removal conditions by using a request body. So our function will look like this:
Putting it all together
Great! Now with our route functionality built out, our routes.js should look like this:
Now that we have that all set up, let’s run our tests again:
And there you have it! We can now insert
get
and delete
from our database!
Things to consider, how to improve
Here are just a few considerations for our API, which can help us improve our system:
- Refactor. Getting a working model isn’t the end result. Now that we have our tests passing, we should look to refactor our code. Making it neater / easier to read will help us and our fellow coworkers prevent any eye-strain when they stumble upon our repository.
- Protection against incorrect request bodies. What if the request body key is incorrect? Or a
POST
request is submitted without a request body at all? Currently we are taking whatever request body there is and trying to insert it into the database, but we could put some checks in to prevent incorrect data from being inserted. findOneAndDelete
will delete something even if not request body is specified. Depending on the circumstance, that may be undesirable.- Improved error handling. Right now we are just sending
Uh Oh! Error!
to the client. What message would be more useful for them to know? - Improved logging. If we are to use this API for production, we would need to improve our logging. Currently we aren’t logging anything about incoming requests, process times, timestamp or which endpoint and method is being called. As our API grows, that information becomes more valuable for debugging.
- Remove hard-coded values. Right now our model is hard-coded to
"fruit"
. What if I wanted to add new data to a new collection?