https://unsplash.com/photos/th3rQu0K3aM
β‘οΈ Github Repo is available here β¬ οΈ
- REST API?
- What I will use for this introduction
- Building the API routes
- Connecting with MongoDB through Mongoose
- Connecting the API to the database
- Finalizing and testing the API
- Conclusion
"We see our customers as invited guests to a party, and we are the hosts. It's our job every day to make every important aspect of the customer experience a little bit better." - Jeff Bezos
REST APIs handle the server side of the web application. That means that the backend is built once and can provide content or data for frontend, mobile, or other server-side apps. A great example is the google calendar API.
REST stands for Representational State Transfer and is a way how a web server should respond to requests. This allows to not only read data, but also do different things like updating, deleting or creating data.
I'm going to build an API that allows to create questions and create, edit, vote and delete answers.
For programming the API it is key to structure the routes properly.
Therefore we need to be able to program routes to:
- look at existing questions and create our own
- read, create, edit, delete answers
- vote on answers
In this example here I will use
Plain JavaScript
- Node.js
- Express (JS framework)
- MongoDB (Database)
- Yarn (package management)
- Visual Studio Code as editor
- Postman (testing APIs)
Following packages are used
- body-parser (for parsing incoming requests)
- express (to make the application run)
- nodemon (restarting server when changes occur)
- mongoose (object data modeling to simplify interactions with MongoDB)
- morgan (HTTP request logger middleware )
- eslint with Airbnb extension (for writing higher quality code)
The tutorial will be structured in:
- building the API routes with express
- modeling data for the API
- communicating with Mongo through Mongoose
- finalizing and testing the API
- install the nodemon package to simplify developing and add the script in the package.json accordingly
- start with creating a basic web server in an express app (check out the docs of express for an example)
- use express middleware to be as flexible as possible
- add the body-parser package to parse requests
- use the parser in a middleware
- create a router.js file to store all routes
- set up GET and POST routes to look at questions and create them
- set up a GET route for specific questions
- for example:
router.get('/', (req, res) => {
res.json({ response: 'a GET request for LOOKING at questions' });
});
router.post('/', (req, res) => {
res.json({
response: 'a POST request for CREATING questions',
body: req.body
});
});
router.get('/:qID', (req, res) => {
res.json({
response: `a GET request for LOOKING at a special answer id: ${req.params.qID}`
});
});
- install the morgan package for logging http requests (helps analyzing your requests in the console)
- set up the POST route for creating answers
- set up the PUT and DELETE route for editing and deleting answers
- set up a POST route for creating voting on answers
- for example:
router.post('/:qID/answers', (req, res) => {
res.json({
response: 'a POST request for CREATING answers',
question: req.params.qID,
body: req.body
});
});
router.put('/:qID/answers/:aID', (req, res) => {
res.json({
response: 'a PUT request for EDITING answers',
question: req.params.qID,
answer: req.params.aID,
body: req.body
});
});
router.delete('/:qID/answers/:aID', (req, res) => {
res.json({
response: 'a DELETE request for DELETING answers',
question: req.params.qID,
answer: req.params.aID,
body: req.body
});
});
router.post('/:qID/answers/:aID/vote-:dec', (req, res) => {
res.json({
response: 'a POST request for VOTING on answers',
question: req.params.qID,
answer: req.params.aID,
vote: req.params.dec,
body: req.body
});
});
- use middleware to catch errors efficiently
- catch 404s and pass to custom error handler to send readable JSON messages (if no error is passed, use 500)
- create an individual validation middleware for voting errors (only allowing up or down voting)
- for example:
app.use((req, res, next) => {
const err = new Error('Not Found');
err.status = 404;
next(err);
});
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.json({
error: {
message: err.message
}
});
});
Structure what kind of data has to be stored in a database and what type of relation the data has. I will use Mongoose to set the data handling for MongoDB. Schemas allow to define the data in JSON format.
In this case this is best implemented using only question objects with answer properties. However, bare in mind, that documents have a storage limit and therefore the amount of answers is limited.
- create a Schema that follows your desired parent-children structure
- build a model with the Schema
- for example:
const AnswerSchema = new Schema({
text: String,
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
votes: { type: Number, default: 0 }
});
const QuestionSchema = new Schema({
text: String,
createdAt: { type: Date, default: Date.now },
answers: [AnswerSchema]
});
const Question = mongoose.model('Question', QuestionSchema);
- answers should be sorted by the newest
- and voting should also be stored in the database
- create a mongoose prehook to sort when saving
- call the child document's parent method to reference the parent document (answer references the question)
- be sure to not use arrow functions when specifying this!
- for example:
const sortAnswers = (a, b) => {
if (a.votes === b.votes) {
return b.updatedAt - a.updatedAt;
}
return b.votes - a.votes;
};
QuestionSchema.pre('save', function (next) {
this.answers.sort(sortAnswers);
next();
});
AnswerSchema.method('update', function (updates, callback) {
Object.assign(this, updates, { updatedAt: new Date() });
this.parent().save(callback);
});
AnswerSchema.method('vote', function (vote, callback) {
if (vote === 'up') {
this.votes += 1;
} else {
this.votes -= 1;
}
this.parent().save(callback);
});
βFor me this part was the hardest one! Don't worry if you don't get it at the first try right. Make sure to study the mongoose docs properly :)
- use the
param
method of the router to trigger callbacks on certain routes (for qID and aID) (see docs) - this way you can always check for errors if the question or answer is not to find
- for example:
router.param('qID', (req, res, next, id) => {
Question.findById(id, (err, doc) => {
if (err) return next(err);
if (!doc) {
err = new Error('Document not found');
err.status = 404;
return next(err);
}
req.question = doc;
return next();
});
});
router.param('aID', (req, res, next, id) => {
req.answer = req.question.answers.id(id);
if (!req.answer) {
err = new Error('Answer not found');
err.status = 404;
return next(err);
}
return next();
});
- for the GET route find the questions in your database
- return the questions
- for the POST route (creating new questions) create a new question with the body of the request and save it to the database in JSON format
- for the GET route on one question just response the specific question
- for example:
router.get('/', (req, res, next) => {
Question.find({}).sort({ createdAt: -1 }).exec((err, questions) => {
if (err) return next(err);
res.json(questions);
});
});
router.post('/', (req, res) => {
const question = new Question(req.body);
question.save((err, question) => {
if (err) return next(err);
res.status(201);
res.json(question);
});
});
router.get('/:qID', (req, res) => {
res.json(req.question);
});
- for the POST route (creating answers) simply push the answer to the answer array in the question document and save it with the mongoose methods as JSON
- for the PUT route (updating the answer) return the new JSON with the
update
method - for the DELETE route (to remove answers) use the
remove
method - for the POST route (to create voting) use middleware to check for the correct expression and use the
vote
method we defined in the model file - for example
router.post('/:qID/answers', (req, res, next) => {
req.question.answers.push(req.body);
req.question.save((err, question) => {
if (err) return next(err);
res.status(201);
res.json(question);
});
});
router.put('/:qID/answers/:aID', (req, res, next) => {
req.answer.update(req.body, (err, result) => {
if (err) return next(err);
res.json(result);
});
});
router.delete('/:qID/answers/:aID', (req, res) => {
req.answer.remove(err => {
req.question.save((err, question) => {
if (err) return next(err);
res.json(question);
});
});
});
router.post(
'/:qID/answers/:aID/vote-:dec',
(req, res, next) => {
if (req.params.dec.search(/^(up|down)$/) === -1) {
const err = new Error(`Not possible to vot for ${req.params.dec}!`);
err.status = 404;
next(err);
} else {
req.vote = req.params.dec;
next();
}
},
(req, res, next) => {
req.answer.vote(req.vote, (err, question) => {
if (err) return next(err);
res.json(question);
});
}
);
At this point the REST API is completely implemented and ready to be consumed.
Check out a timelapse of my implementation: (click on the picture ;) )
To test whether all routes are doing what they should I'll use the chrome extension of Postman. The interface allows to test all HTTP methods efficiently.
On a note: Instead of testing all routes manually automated tests can also be set up. ;)
Cross Origin Resource Sharing allows the browser though headers to access resources from a different domain. Due to security risk this is restricted.
Check out this article for an introduction to CORS.
We have to build a middleware that allows domains to consume the API
- set the header to allow access to all origins
- allow HTTP methods
- for example:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
if (req.method === 'Options') {
res.header('Access-Control-Allow-Methods', 'PUT, POST, DELETE');
return res.status(200).json({});
}
});
The working API can be connected to all types of frontend frameworks. Maybe I'll do that and write another article. :)
β‘οΈ Github Repo is available here β¬ οΈ
As we can see a REST API is a great tool for setting up basic backend microservices. For actually implementing the API it is necessary to fully understand the database you are working with and it's interaction with routes. In this case this was mongoose.
Also keep in mind, that for production you will certainly need User Authentication and Authorization to manage identity and access rights.
If you gained something from this article let me know with a comment or heart. Make sure to follow for more :)