banner



How To Build A Scubscriptio Service With Nodejs

Awarding programming interfaces (APIs) are everywhere. They enable software to communicate with other pieces of software—internal or external—consistently, which is a key ingredient in scalability, not to mention reusability.

It'due south quite common nowadays for online services to have public-facing APIs. These enable other developers to easily integrate features like social media logins, credit card payments, and behavior tracking. The de facto standard they utilize for this is called REpresentational Country Transfer (REST).

While a multitude of platforms and programming languages can be used for the task—e.g., ASP.NET Core, Laravel (PHP), or Bottle (Python)—in this tutorial, we'll build a bones only secure Balance API dorsum end using the following stack:

  • Node.js, which the reader should already have some familiarity with
  • Express, which vastly simplifies edifice out common web server tasks under Node.js and is standard fare in building a REST API back finish
  • Mongoose, which will connect our back terminate to a MongoDB database

Developers following this tutorial should also be comfy with the last (or control prompt).

Note: We won't embrace a front end-end codebase here, but the fact that our back cease is written in JavaScript makes it convenient to share lawmaking—object models, for instance—throughout the total stack.

Beefcake of a REST API

REST APIs are used to access and manipulate data using a common set of stateless operations. These operations are integral to the HTTP protocol and represent essential create, read, update, and delete (Grime) functionality, although not in a clean one-to-i style:

  • POST (create a resource or generally provide data)
  • Become (call up an alphabetize of resource or an private resource)
  • PUT (create or replace a resource)
  • PATCH (update/modify a resource)
  • DELETE (remove a resource)

Using these HTTP operations and a resource proper noun as an address, we can build a Residue API by creating an endpoint for each operation. And past implementing the blueprint, we will have a stable and hands understandable foundation enabling us to evolve the lawmaking chop-chop and maintain it afterward. Every bit mentioned before, the same foundation will exist used to integrate third-political party features, most of which likewise use Residue APIs, making such integration faster.

For at present, let'south get-go creating our secure Rest API using Node.js!

In this tutorial, we are going to create a pretty common (and very practical) REST API for a resource chosen users.

Our resources will have the following bones structure:

  • id (an auto-generated UUID)
  • firstName
  • lastName
  • email
  • countersign
  • permissionLevel (what is this user allowed to do?)

And we will create the following operations for that resource:

  • POST on the endpoint /users (create a new user)
  • GET on the endpoint /users (list all users)
  • Become on the endpoint /users/:userId (get a specific user)
  • PATCH on the endpoint /users/:userId (update the data for a specific user)
  • DELETE on the endpoint /users/:userId (remove a specific user)

We will too exist using JSON web tokens (JWTs) for access tokens. To that terminate, nosotros will create some other resources called auth that volition expect a user's email and countersign and, in return, will generate the token used for authentication on certain operations. (Dejan Milosevic's great article on JWT for secure REST applications in Java goes into further detail almost this; the principles are the same.)

Rest API Tutorial Setup

Showtime of all, make certain that you have the latest Node.js version installed. For this commodity, I'll be using version 14.9.0; it may also piece of work on older versions.

Next, make sure that yous have MongoDB installed. We won't explain the specifics of Mongoose and MongoDB that are used here, but to get the nuts running, simply start the server in interactive way (i.due east., from the command line as mongo) rather than as a service. That'due south considering, at one point in this tutorial, we'll need to collaborate with MongoDB directly rather than via our Node.js code.

Note: With MongoDB, in that location'southward no need to create a specific database similar in that location might be in some RDBMS scenarios. The first insert call from our Node.js code will trigger its creation automatically.

This tutorial does not contain all of the lawmaking necessary for a working projection. It'due south intended instead that you clone the companion repo and only follow along the highlights as you read through—but you can also copy in specific files and snippets from the repo as needed, if you prefer.

Navigate to the resulting residue-api-tutorial/ folder in your concluding. You'll see that our project contains three module folders:

  • common (handling all shared services, and information shared betwixt user modules)
  • users (everything regarding users)
  • auth (handling JWT generation and the login catamenia)

At present, run npm install (or yarn if yous have it.)

Congratulations, you now take all of the dependencies and setup required to run our elementary Residue API back end.

Creating the User Module

We will exist using Mongoose, an object data modeling (ODM) library for MongoDB, to create the user model within the user schema.

Get-go, we need to create the Mongoose schema in /users/models/users.model.js:

          const userSchema = new Schema({    firstName: Cord,    lastName: String,    email: String,    password: Cord,    permissionLevel: Number });                  

One time we define the schema, nosotros tin hands attach the schema to the user model.

          const userModel = mongoose.model('Users', userSchema);                  

Afterwards that, nosotros tin employ this model to implement all the CRUD operations that we want inside our Express endpoints.

Let's start with the "create user" performance past defining the road in users/routes.config.js:

          app.mail service('/users', [    UsersController.insert ]);                  

This is pulled into our Express app in the chief alphabetize.js file. The UsersController object is imported from our controller, where nosotros hash the password appropriately, defined in /users/controllers/users.controller.js:

          exports.insert = (req, res) => {    let salt = crypto.randomBytes(16).toString('base64');    let hash = crypto.createHmac('sha512',salt)                                     .update(req.trunk.password)                                     .digest("base64");    req.torso.countersign = salt + "$" + hash;    req.body.permissionLevel = 1;    UserModel.createUser(req.body)        .so((result) => {            res.status(201).send({id: result._id});        }); };                  

At this point, we tin can exam our Mongoose model by running the server (npm outset) and sending a POST request to /users with some JSON data:

          {    "firstName" : "Marcos",    "lastName" : "Silva",    "e-mail" : "marcos.henrique@toptal.com",    "password" : "s3cr3tp4sswo4rd" }                  

There are several tools you can use for this. Insomnia (covered below) and Postman are popular GUI tools, and curl is a mutual CLI pick. You can even but use JavaScript, e.g., from your browser'southward built-in development tools console:

          fetch('http://localhost:3600/users', {         method: 'POST',         headers: {             "Content-blazon": "application/json"         },         body: JSON.stringify({             "firstName": "Marcos",             "lastName": "Silva",             "email": "marcos.henrique@toptal.com",             "countersign": "s3cr3tp4sswo4rd"         })     })     .and then(function(response) {         return response.json();     })     .then(part(data) {         panel.log('Request succeeded with JSON response', data);     })     .catch(part(error) {         console.log('Asking failed', error);     });                  

At this point, the outcome of a valid post will be just the id from the created user: { "id": "5b02c5c84817bf28049e58a3" }. Nosotros need to too add the createUser method to the model in users/models/users.model.js:

          exports.createUser = (userData) => {     const user = new User(userData);     return user.salve(); };                  

All set, now we demand to encounter if the user exists. For that, we are going to implement the "get user by id" characteristic for the post-obit endpoint: users/:userId.

First, we create a route in /users/routes/config.js:

          app.become('/users/:userId', [     UsersController.getById ]);                  

Then, nosotros create the controller in /users/controllers/users.controller.js:

          exports.getById = (req, res) => {    UserModel.findById(req.params.userId).and so((upshot) => {        res.status(200).send(outcome);    }); };                  

And finally, add the findById method to the model in /users/models/users.model.js:

          exports.findById = (id) => {     return User.findById(id).and then((result) => {         consequence = event.toJSON();         delete event._id;         delete result.__v;         return outcome;     }); };                  

The response will exist like this:

          {    "firstName": "Marcos",    "lastName": "Silva",    "electronic mail": "marcos.henrique@toptal.com",    "countersign": "Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==",    "permissionLevel": one,    "id": "5b02c5c84817bf28049e58a3" }                  

Notation that nosotros can see the hashed password. For this tutorial, we are showing the password, but the obvious best practise is never to reveal the password, even if information technology has been hashed. Another matter we can encounter is the permissionLevel, which nosotros will use to handle the user permissions later on.

Repeating the design laid out above, we can now add the functionality to update the user. Nosotros will use the PATCH performance since it will enable us to send merely the fields we desire to change. The route volition, therefore, be PATCH to /users/:userid, and we'll be sending any fields nosotros want to change. We will also need to implement some extra validation since changes should be restricted to the user in question or an admin, and only an admin should be able to change the permissionLevel. We'll skip that for now and get dorsum to it once nosotros implement the auth module. For at present, our controller will look like this:

          exports.patchById = (req, res) => {    if (req.body.password){        let common salt = crypto.randomBytes(16).toString('base64');        allow hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64");        req.trunk.countersign = salt + "$" + hash;    }    UserModel.patchUser(req.params.userId, req.body).then((result) => {            res.status(204).ship({});    }); };                  

By default, we volition send an HTTP code 204 with no response torso to indicate that the request was successful.

And nosotros'll demand to add the patchUser method to the model:

          exports.patchUser = (id, userData) => {     return User.findOneAndUpdate({         _id: id     }, userData); };                  

The user list will exist implemented equally a Go at /users/ by the following controller:

          exports.listing = (req, res) => {    let limit = req.query.limit && req.query.limit <= 100 ? parseInt(req.query.limit) : x;    let page = 0;    if (req.query) {        if (req.query.page) {            req.query.page = parseInt(req.query.page);            page = Number.isInteger(req.query.page) ? req.query.page : 0;        }    }    UserModel.list(limit, folio).then((issue) => {        res.condition(200).ship(upshot);    }) };                  

The corresponding model method will be:

          exports.list = (perPage, page) => {     return new Hope((resolve, turn down) => {         User.find()             .limit(perPage)             .skip(perPage * page)             .exec(function (err, users) {                 if (err) {                     reject(err);                 } else {                     resolve(users);                 }             })     }); };                  

The resulting list response volition have the following structure:

          [    {        "firstName": "Marco",        "lastName": "Silva",        "email": "marcos.henrique@toptal.com",        "password": "z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==",        "permissionLevel": one,        "id": "5b02c5c84817bf28049e58a3"    },    {        "firstName": "Paulo",        "lastName": "Silva",        "email": "marcos.henrique2@toptal.com",        "password": "wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==",        "permissionLevel": 1,        "id": "5b02d038b653603d1ca69729"    } ]                  

And the last part to be implemented is the DELETE at /users/:userId.

Our controller for deletion will be:

          exports.removeById = (req, res) => {    UserModel.removeById(req.params.userId)        .so((result)=>{            res.condition(204).ship({});        }); };                  

Same as before, the controller will return HTTP code 204 and no content torso as confirmation.

The respective model method should look like this:

          exports.removeById = (userId) => {     return new Promise((resolve, reject) => {         User.deleteMany({_id: userId}, (err) => {             if (err) {                 refuse(err);             } else {                 resolve(err);             }         });     }); };                  

Nosotros now have all the necessary operations for manipulating the user resources, and we're done with the user controller. The principal thought of this code is to requite you the cadre concepts of using the REST blueprint. We'll need to return to this code to implement some validations and permissions to it, but get-go, we'll need to start edifice our security. Let's create the auth module.

Creating the Auth Module

Earlier we can secure the users module by implementing the permission and validation middleware, we'll demand to be able to generate a valid token for the electric current user. We volition generate a JWT in response to the user providing a valid email and password. JWT is a remarkable JSON web token that you can use to take the user securely make several requests without validating repeatedly. It usually has an expiration fourth dimension, and a new token is recreated every few minutes to keep the communication secure. For this tutorial, though, we will forgo refreshing the token and continue it simple with a single token per login.

First, nosotros will create an endpoint for POST requests to /auth resources. The request body volition contain the user e-mail and countersign:

          {    "electronic mail" : "marcos.henrique2@toptal.com",    "password" : "s3cr3tp4sswo4rd2" }                  

Earlier we appoint the controller, we should validate the user in /authority/middlewares/verify.user.middleware.js:

          exports.isPasswordAndUserMatch = (req, res, next) => {    UserModel.findByEmail(req.body.email)        .and so((user)=>{            if(!user[0]){                res.status(404).send({});            }else{                let passwordFields = user[0].password.split('$');                permit common salt = passwordFields[0];                permit hash = crypto.createHmac('sha512', salt).update(req.torso.password).digest("base64");                if (hash === passwordFields[1]) {                    req.body = {                        userId: user[0]._id,                        email: user[0].electronic mail,                        permissionLevel: user[0].permissionLevel,                        provider: 'email',                        name: user[0].firstName + ' ' + user[0].lastName,                    };                    render next();                } else {                    return res.condition(400).send({errors: ['Invalid email or countersign']});                }            }        }); };                  

Having done that, nosotros can movement on to the controller and generate the JWT:

          exports.login = (req, res) => {    try {        let refreshId = req.trunk.userId + jwtSecret;        let salt = crypto.randomBytes(16).toString('base64');        let hash = crypto.createHmac('sha512', common salt).update(refreshId).assimilate("base64");        req.trunk.refreshKey = common salt;        allow token = jwt.sign(req.body, jwtSecret);        let b = Buffer.from(hash);        permit refresh_token = b.toString('base64');        res.status(201).send({accessToken: token, refreshToken: refresh_token});    } grab (err) {        res.condition(500).send({errors: err});    } };                  

Fifty-fifty though we won't be refreshing the token in this tutorial, the controller has been ready to enable such generation to make it easier to implement it in subsequent evolution.

All we need now is to create the road and invoke the appropriate middleware in /authorization/routes.config.js:

                      app.post('/auth', [         VerifyUserMiddleware.hasAuthValidFields,         VerifyUserMiddleware.isPasswordAndUserMatch,         AuthorizationController.login     ]);                  

The response will contain the generated JWT in the accessToken field:

          {    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY",    "refreshToken": "U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ==" }                  

Having created the token, we can utilize it inside the Authorization header using the form Bearer ACCESS_TOKEN.

Creating Permissions and Validations Middleware

The first affair we should define is who can use the users resource. These are the scenarios that we'll need to handle:

  • Public for creating users (registration process). We will non use JWT for this scenario.
  • Individual for the logged-in user and for admins to update that user.
  • Private for admin only for removing user accounts.

Having identified these scenarios, we will first require a middleware that e'er validates the user if they are using a valid JWT. The middleware in /common/middlewares/auth.validation.middleware.js tin be as simple as:

          exports.validJWTNeeded = (req, res, next) => {     if (req.headers['authority']) {         endeavour {             permit potency = req.headers['authorization'].split(' ');             if (say-so[0] !== 'Bearer') {                 return res.condition(401).transport();             } else {                 req.jwt = jwt.verify(potency[ane], underground);                 return next();             }         } take hold of (err) {             return res.status(403).send();         }     } else {         return res.status(401).send();     } };                  

Nosotros volition use HTTP error codes for handling request errors:

  • HTTP 401 for an invalid request
  • HTTP 403 for a valid request with an invalid token, or valid token with invalid permissions

We can use the bitwise AND operator (bitmasking) to command the permissions. If we set each required permission as a power of 2, nosotros tin can care for each flake of the 32-bit integer as a single permission. An admin tin then have all permissions by setting their permission value to 2147483647. That user could and so have access to whatever route. As another instance, a user whose permission value was fix to 7 would have permissions to the roles marked with $.25 for values 1, ii, and 4 (2 to the power of 0, one, and 2).

The middleware for that would look like this:

          exports.minimumPermissionLevelRequired = (required_permission_level) => {    render (req, res, next) => {        let user_permission_level = parseInt(req.jwt.permission_level);        allow user_id = req.jwt.user_id;        if (user_permission_level & required_permission_level) {            render adjacent();        } else {            return res.status(403).transport();        }    }; };                  

The middleware is generic. If the user permission level and the required permission level coincide in at least one bit, the issue will be greater than zero, and we can allow the activity keep; otherwise, the HTTP lawmaking 403 will be returned.

Now, we demand to add the authentication middleware to the user's module routes in /users/routes.config.js:

          app.post('/users', [    UsersController.insert ]); app.get('/users', [    ValidationMiddleware.validJWTNeeded,    PermissionMiddleware.minimumPermissionLevelRequired(PAID),    UsersController.list ]); app.go('/users/:userId', [    ValidationMiddleware.validJWTNeeded,    PermissionMiddleware.minimumPermissionLevelRequired(Complimentary),    PermissionMiddleware.onlySameUserOrAdminCanDoThisAction,    UsersController.getById ]); app.patch('/users/:userId', [    ValidationMiddleware.validJWTNeeded,    PermissionMiddleware.minimumPermissionLevelRequired(Gratis),    PermissionMiddleware.onlySameUserOrAdminCanDoThisAction,    UsersController.patchById ]); app.delete('/users/:userId', [    ValidationMiddleware.validJWTNeeded,    PermissionMiddleware.minimumPermissionLevelRequired(ADMIN),    UsersController.removeById ]);                  

This concludes the basic evolution of our Rest API. All that remains to be done is to exam it all out.

Running and Testing with Indisposition

Insomnia is a decent REST client with a practiced free version. The best practice is, of course, to include lawmaking tests and implement proper mistake reporting in the project, simply tertiary-party REST clients are cracking for testing and implementing third-party solutions when error reporting and debugging the service is not available. We'll be using it here to play the role of an application and go some insight into what is going on with our API.

To create a user, we only need to POST the required fields to the advisable endpoint and store the generated ID for subsequent utilise.

Request with the appropriate data for creating a user

The API will reply with the user ID:

Confirmation response with userID

We can now generate the JWT using the /auth/ endpoint:

Request with login data

We should get a token as our response:

Confirmation containing the corresponding JSON Web Token

Take hold of the accessToken, prefix information technology with Bearer (remember the infinite), and add it to the asking headers under Authorization:

Setting up the headers to transfer contain the authenticating JWT

If we don't practise this now that nosotros accept implemented the permissions middleware, every request other than registration would be returning HTTP code 401. With the valid token in place, though, we go the following response from /users/:userId:

Response listing the data for the indicated user

Also, as was mentioned earlier, we are displaying all fields, for educational purposes and for sake of simplicity. The password (hashed or otherwise) should never exist visible in the response.

Let'due south try to get a list of users:

Request for a list of all users

Surprise! Nosotros get a 403 response.

Action refused due to lack of appropriate permission level

Our user does not have the permissions to access this endpoint. We will need to modify the permissionLevel of our user from 1 to 7 (or fifty-fifty 5 would practise, since our complimentary and paid permissions levels are represented equally 1 and four, respectively.) We tin practice this manually in MongoDB, at its interactive prompt, like this (with the ID changed to your local event):

          db.users.update({"_id" : ObjectId("5b02c5c84817bf28049e58a3")},{$set up:{"permissionLevel":v}})                  

Then, nosotros need to generate a new JWT.

Later on that is washed, we become the proper response:

Response with all users and their data

Side by side, let'due south test the update functionality past sending a PATCH request with some fields to our /users/:userId endpoint:

Request containing partial data to be updated

We expect a 204 response as confirmation of a successful operation, but we can asking the user one time again to verify.

Response after successful change

Finally, we demand to delete the user. Nosotros'll demand to create a new user as described above (don't forget to note the user ID) and brand sure that we take the appropriate JWT for an admin user. The new user will need their permissions set to 2053 (that's 2048—ADMIN—plus our earlier 5) to exist able to likewise perform the delete performance. With that washed and a new JWT generated, we'll take to update our Say-so asking header:

Request setup for deleting a user

Sending a DELETE request to /users/:userId, nosotros should get a 204 response as confirmation. Nosotros can, over again, verify by requesting /users/ to listing all existing users.

Side by side Steps for Your REST API

With the tools and methods covered in this tutorial, you should now be able to create uncomplicated and secure REST APIs on Node.js. A lot of best practices that are not essential to the process were skipped, and then don't forget to:

  • Implement proper validations (eastward.g., make certain that user email is unique)
  • Implement unit testing and fault reporting
  • Prevent users from changing their own permission level
  • Prevent admins from removing themselves
  • Prevent disclosure of sensitive data (east.g., hashed passwords)
  • Move the JWT hugger-mugger from common/config/env.config.js to an off-repo, non-environment-based hole-and-corner distribution mechanism

Ane final practice for the reader tin exist to convert the codebase from its use of JavaScript promises over to the async/await technique.

For those of you who might be interested, there is at present besides a TypeScript version of the project available.

How To Build A Scubscriptio Service With Nodejs,

Source: https://www.toptal.com/nodejs/secure-rest-api-in-nodejs

Posted by: healeycoled1948.blogspot.com

0 Response to "How To Build A Scubscriptio Service With Nodejs"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel