In this article, we’ll build a REST API with Node.js and Express and protect one endpoint from arbitrary requests.
At first, we will develop an unsecured API, then we will add a own validation and analyze the shortcomings of it, finally we’ll use celebrate , a joi middleware for Express, to implement a completely flexible solution.
In order to bring to life our API, we need to setup our new project:
$ mkdir swamp-api $ cd swamp-api
Our Swamp API will allow us to register alligator nests and consult a list of them.
Inside our recently created directory, start an empty Node.js project:
$ npm init -y
As a last step we add to our project express
and body-parser
:
$ npm install express body-parser --save
Now we're ready to build the first version of our API!
Alligator.io recommends ⤵
:point_right: Learn Node, a video course by Wes BosYou'll learn things like user auth , database storage , building a REST API and uploading images in Node.js by building a real world restaurant application.
On this first section we’ll accomplish these steps:
GET /health
endpoint cURL
GET /nest
and POST /nest
endpoints
The next snippet presents a very basic server with a GET /health
endpoint:
server.js
// Require express and initialize a new app from it const express = require('express'); const app = express(); const PORT = 3000; // Register a new GET endpoint, accessible through health route // The response will be an json object with {ok: true} as content app.get('/health', (req, res) => res.json({ok: true})); // Start server on 3000 port // Console.log will output the message when server is listening app.listen(PORT, () => console.log(`Swamp API listening on port ${PORT}`));
Start it:
$ node server.js
The output after initialization should be Swamp API listening on port 3000
.
On another terminal, try to access the GET /health
endpoint:
$ curl -s http://localhost:3000
We should get {"ok": true}
as response.
In order to get the API responses with a pretty format, I recommend a tool called jq
. Simply pipe the previous command to it, like this: $ curl -s http://localhost:3000 | jq .
It’s time to add the core functionality: the /nest
GET
and POST
endpoints. With these endpoints we’ll be able to retrieve the nests and add more.
server.js
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); const PORT = 3000; app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: false})); let nests = [ { momma: 'swamp_princess', eggs: 40, temperature: 31 }, { momma: 'mrs.chompchomp', eggs: 37, temperature: 32.5 } ]; app.get('/health', (req, res) => res.json({ok: true})); app.get('/nest', (req, res) => res.json(nests)); app.post('/nest', (req, res) => { const newNest = req.body; nests.push(newNest); return res.json(newNest); });
First we require the body-parser
module, next we register it on the Express app, after that we create a nests
array to use them as a sample response. Lastly, we add two new endpoints ( GET
and POST
):
nests
array nests
array and returns the new nest to the client
Everything is ready to try, we’ll start testing things out with the GET /nest
endpoint:
$ curl -s http://localhost:3000/nest | jq .
The output should be a the list of nests we declared.
Now we can go with POST /nest
:
$ curl -X POST \ -H "Content-Type: application/json" \ -d '{"momma": "lady.sharp.tooth", "eggs": 42, "temperature": 34}'\ -s http://localhost:3000/nest | jq .
The output should now be the recently added nest.
Just for the sake of completeness we’ll hit GET /nest
again:
$ curl -s http://localhost:3000/nest | jq .
An updated list of nests should be printed.
The first version of our service is completed, but if we care about data consistency we have a major problem on our hands.
We can add new nests trough POST /nest
and the current implementation doesn’t validate that input, so we are accepting all bodies and pushing them to our list, like this:
$ curl -X POST \ -H "Content-Type: application/json" \ -d '{"eggs": 31.4, "temperature": "VERY HIGH"}'\ -s http://localhost:3000/nest | jq .
$ curl -s http://localhost:3000/nest | jq .
The invalid nest now is part of the list.
Based on the previous server implementation, we’ll follow these steps:
POST /nest
Here is the new version of POST /nest
endpoint, we’ll update only the POST /nest
endpoint:
server.js
app.post('/nest', (req, res) => { const newNest = req.body; const { momma, eggs, temperature } = newNest; if (!momma || !eggs) { return res.status(400).json({error: 'Not valid nest'}); } nests.push(newNest); return res.json(newNest); });
Here we’re not implementing a complete solution, this is just an example to illustrate how difficult it could be to come up with a validation that cares about more than one scenario.
We should be focused on what our API is needed for not put our efforts on how and when to validate input information.
And that’s where celebrate
enters and the next section is all about it and how to implement it.
celebrate
allows us implement a flexible validation based on a predefined schema.
For this purpose, a schema is a JavaScript object that describes how requests (URL parameters, bodies, headers, etc.) must be, an example:
const schema = { body: { name: Joi.string().required(), age: Joi.number().integer().required() } };
This schema defines (using joi
, library that celebrate
wraps) that a valid body is only one that has name
as string and age
as integer, anything outside of this will trigger an error.
Note that the schema is an object that's inside another object called body
, this name is required as it is in order to check the body
object on the request object.
With this information, we define here the steps to implement celebrate:
celebrate POST /nest celebrate
$ npm install --save celebrate
Here is the updated version, this time using the celebrate
middleware:
server.js
const express = require('express'); const bodyParser = require('body-parser'); const { celebrate, Joi } = require('celebrate'); const app = express(); const PORT = 3000; let nests = [ { momma: 'swamp_princess', eggs: 40, temperature: 31 }, { momma: 'mrs.chompchomp', eggs: 37, temperature: 32.5 } ]; const nestSchema = { body: { momma: Joi.string().required(), eggs: Joi.number().integer(), temperature: Joi.number() } }; app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: false})); app.get('/health', (req, res) => res.json({ok: true})); app.get('/nest', (req, res) => res.json(nests)); app.post('/nest', celebrate(nestSchema), (req, res) => { const newNest = req.body; nests.push(newNest); return res.json(newNest); }); app.use((error, req, res, next) => { if (error.joi) { return res.status(400).json({error: error.joi.message}); } return res.status(500).send(error) }); app.listen(PORT, () => console.log(`Swamp API listening on port ${PORT}`));
We start by requiring the celebrate
and Joi
from the celebrate
module.
After that we define a new schema called nestSchema
that has momma
and eggs
as required values with defined types (a string and an integer, respectively), in addition to this we add a temperature
field, as a number, so it will accept an integer or a float.
The next update is for the POST /nest
endpoint, we can see that before pushing our new nest we are calling the celebrate
function with the schema as argument, and internally celebrate will handle the validation for us.
The last update is the addition of a error handler middleware, because celebrate
is in charge of everything it needs a way to catch the possible errors, these errors will have a joi
property as true
so we can detect them.
We’re ready to test it again, first with an invalid body on POST /nest
:
$ curl -X POST \ -H "Content-Type: application/json" \ -d '{"eggs": 31.4, "temperature": "VERY HIGH"}'\ -s http://localhost:3000/nest | jq .
The message we should receive will look like this:
{ "error": "child \"momma\" fails because [\"momma\" is required]" }
Remember on the error handler that we check if it has the joi
property as true
? The error message that we have here is the one that celebrate
prepared for us. Now we know that momma
field was required, and we can update our request.
$ curl -X POST \ -H "Content-Type: application/json" \ -d '{"momma": "msg.alligator", "eggs": 31.4, "temperature": 33}'\ -s http://localhost:3000/nest | jq .
{ "error": "child \"eggs\" fails because [\"eggs\" must be an integer]" }
Now celebrate
is alerting us that the eggs
field must be an integer, so we will update the request again.
$ curl -X POST \ -H "Content-Type: application/json" \ -d '{"momma": "Mrs Alligator", "eggs": 31, "temperature": 33}'\ -s http://localhost:3000/nest | jq .
{ "momma": "Mrs Alligator", "eggs": 31, "temperature": 33 }
Finally, we have as result the nest we pushed, that means that the request was valid and will be pushed to the nests
list, we can verify with a request to GET /nest
:
$ curl -s http://localhost:3000/nest | jq .
You've made it!, the Swamp API will have consistent information.
The code presented here has a lot of room for improvement.
I highly encourage you to follow the official documentation and enhance this server, limiting the minimum and maximum for eggs
and temperature
could be good next steps!
我来评几句
登录后评论已发表评论数()