Bắt đầu Nodejs - Mongoose API (Authentication - CRUD) cho người mới học

Trong bài viết này, mình và các bạn sẽ cùng thực hiện Authentication và CRUD của 1 ứng dụng Nodejs API một cách đơn giản, phù hợp với những bạn mới học và mới bắt đầu tiếp cận với nodejs.

1. Chuẩn bị

  • JavaScript
  • Node.js
  • Postman
  • Express (JS framework)
  • MongoDB (Database)
  • Npm (quản lý các package)
  • Visual Studio Code (hoặc Sublime Text)

2. Tạo Project

Đầu tiên hãy tạo 1 thư mục để triển:

~~$ mkdir MyNodeProject
~~$ cd MyNodeProject
...:~/MyNodeProject$ npm init

Đến đây bạn đã có 1 file package.json:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Tiếp theo bạn cần thiết lập Express bằng cách:

...:~/MyNodeProject$ npm i express

File package.json của bạn sẽ có dạng như này:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.4"
  }
}

Rồi, bạn tạo 1 file index.js giống như main ở trong package.json

...:~/MyNodeProject$ touch index.js

Trong file index.js sẽ cần thiết lập như sau:

const express = require('express')
const app = express()
const PORT = 8797
app.use('/', (req, res) => {
    res.json({"mess": "Hello Would!"})
})

app.listen(PORT, () => {console.log("Server started on http://localhost:"+PORT)})

module.exports = app;

Thử start nhé:

...:~/MyNodeProject$ node index.js

Và mở Postman chạy http://localhost:8797, bạn sẽ có được:

Để server tự động start lại sau khi có sự thay đổi thì bạn cần cài nodemon:

...:~/MyNodeProject$ npm i nodemon

Và thiết lập package.json như sau:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "devstart": "nodemon run index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.4",
    "nodemon": "^1.18.10"
  }
}

Chạy:

...:~/MyNodeProject$ npm run devstart

Các package cần dùng:

  • body-parser (parse các request tới server)
  • express (làm cho ứng dụng chạy)
  • nodemon (restart khi có thay đổi xảy ra)
  • mongoose (mô hình hóa object data để đơn giản hóa các tương tác với MongoDB)
  • bcrypt (hashing và salting passwords)
  • express session (xử lý sessions)
  • connect-mongo (lưu trữ session trong MongoDB)
  • dotenv (sử dụng .env)
  • express-validator
  • morgan
...:~/MyNodeProject$ npm i body-parser mongoose bcrypt express session connect-mongo dotenv express-validator morgan

3. Authentication

3.1. Connect MongoDB

Ở bài này mình sẽ kết nối db ở https://mlab.com/

Bấm CONTINUE và đặt DATABASE NAME là mynodeproject_db

Đến đây, bạn hãy tạo 1 database user, và bạn đc cũng cấp 1 link connect db:

mongodb://<dbuser>:<dbpassword>@ds111549.mlab.com:11549/mynodeproject_db

Quay trở lại với project, tạo 1 file .env ngang hàng với index.js

DB_URL = mongodb://<dbuser>:<dbpassword>@ds111549.mlab.com:11549/mynodeproject_db
PORT = 8797

Thay thế <dbuser><dbpassword> bằng database user và password bạn vừa tạo.

Rồi, trong file index.js

const express = require('express')
const dotenv = require('dotenv')
const mongoose = require('mongoose')

const app = express()
const PORT = process.env.PORT || 8797
const db = mongoose.connection;

dotenv.config()

//connect db
mongoose.connect(process.env.DB_URL, { useNewUrlParser: true }).then(() => console.log('DB Connected!'));
db.on('error', (err) => {
    console.log('DB connection error:', err.message);
})

app.use('/', (req, res) => {
    res.json({"mess": "Hello Would!"})
})

app.listen(PORT, () => {console.log("Server started on http://localhost:"+PORT)})

module.exports = app;

Chạy lại npm run devstart để kiểm tra kết nối.

3.2. Register User

3.2.1. Models

Từ thư mục root, tạo src/models/UserModels.js

const mongoose = require('mongoose')
const Schema = mongoose.Schema

const userSchema = new Schema({
    email: {type: String, unique: true, required: true, trim: true},
    username: {type: String, required: true, trim: true, minlength: 2},
    role: {type: String, enum: ['admin', 'customer']},
    password: {type: String, required: true, trim: true, minlength: 6},
    password_confirm: {type: String, required: true, trim: true, minlength: 6},
});

module.exports = mongoose.model('User', userSchema)

3.2.2. Controllers

Tại src/controllers/UserControllers.js

const User = require('../models/UserModels')
const bcrypt = require('bcrypt')

exports.register = function(req, res, next){    
    User.findOne({email: req.body.email}, (err, user) => {
        if(user == null){ //Kiểm tra xem email đã được sử dụng chưa
            bcrypt.hash(req.body.password, 10, function(err, hash){ //Mã hóa mật khẩu trước khi lưu vào db
                if (err) {return next(err);}
                const user = new User(req.body)
                user.role = ['customer'] //sau khi register thì role auto là customer
                user.password = hash;
                user.password_confirm = hash;
                user.save((err, result) => {
                    if(err) {return res.json({err})}
                    res.json({user: result})
                })
            })
        }else{
            res.json({err: 'Email has been used'})
        }
    })
}

3.2.3. Validator

Tạo src/validators/validator.js

const User = require('../models/UserModels')

exports.UserValidator = function(req, res, next){
    //name
    req.check('email', 'Invalid email.').isEmail();
    req.check('email', 'Email is required.').not().isEmpty();
    req.check('username', 'Username is required.').not().isEmpty();
    req.check('username', 'Username must be more than 1 characters').isLength({min:2});
    req.check('password', 'Password is required.').not().isEmpty();
    req.check('password', 'Password must be more than 6 characters').isLength({min:6});
    req.check('password_confirm', 'Password confirm is required.').not().isEmpty();
    req.check('password_confirm','Password mismatch').equals(req.body.password);

    //check for errors
    const errors = req.validationErrors();
    if(errors){
        const firstError = errors.map(error => error.msg)[0];
        return res.status(400).json({error: firstError});
    }
    next();
}

3.2.4. Routes

Tạo src/routes/routes.js

const express = require('express')
const router = express.Router()
const {register} = require('../controllers/UserControllers')
const {UserValidator} = require('../validators/validator')

router.post('/register', UserValidator, register)

module.exports = router;

3.2.5. Cấu hình lại index.js

const express = require('express')
const dotenv = require('dotenv')
const morgan = require('morgan')
const bodyParser = require('body-parser')
const expressValidator = require('express-validator')
const routes = require('./src/routes/routes')
const mongoose = require('mongoose')

const app = express()
const PORT = process.env.PORT || 8797
const db = mongoose.connection;

dotenv.config()

//connect db
mongoose.set('useCreateIndex', true)
mongoose.connect(process.env.DB_URL, { useNewUrlParser: true }).then(() => console.log('DB Connected!'));
db.on('error', (err) => {
    console.log('DB connection error:', err.message);
})

app.use(morgan("dev"))
app.use(bodyParser.json())
app.use(expressValidator())

app.use('/', routes)

app.listen(PORT, () => {console.log("Server started on http://localhost:"+PORT)})

module.exports = app;

3.2.6. Test

3.3. Login

3.3.1. index.js

/*
...
*/
const session = require('express-session')
const MongoStore = require('connect-mongo')(session);
/*
...
*/
app.use(session({
    secret: 'work hard',
    resave: true,
    saveUninitialized: false,
    store: new MongoStore({
        mongooseConnection: db
    })
}));
/*
...
*/

3.3.2. Controllers

exports.login = function(req, res){
    User.findOne({email: req.body.email}).exec(function(err, user){
        if(err) {
            return res.json({err})
        }else if (!user){
            return res.json({err: 'Username and Password are incorrect'})
        }
        bcrypt.compare(req.body.password, user.password, (err, result) => {
            if(result === true){
                req.session.user = user
                res.json({
                    user: user,
                    "login": "success"
                })
            }else{
                return res.json({err: 'Username and Password are incorrect'})
            }
        })
    })
}

3.3.3. Routes

/*
...
*/
const {register, login} = require('../controllers/UserControllers')

function requiresLogout(req, res, next){
    if (req.session && req.session.user) {
        return res.json({err: 'You must be Logout in to Login continue'});        
    } else {
        return next();
    }
}
router.post('/login', requiresLogout, login)
/*
...
*/

3.4. Logout

3.4.1. Controllers

exports.logout = function(req, res){
    if (req.session) {
        // delete session object
        req.session.destroy(function(err) {
            if(err) {
                return res.json({err});
            } else {
                return res.json({'logout': "Success"});
            }
        });
    }
}

3.4.2. Routes

/*
...
*/
const {register, login, logout} = require('../controllers/UserControllers')

function requiresLogin(req, res, next) {
    if (req.session && req.session.user) {
        return next();
    } else {
        return res.json({err: 'You must be logged in to view this page.'});
    }
}

router.get('/logout', requiresLogin, logout)
/*
...
*/

4. CRUD posts

4.1. Models

//  src/models/PostModels.js


const mongoose = require('mongoose')
const Schema = mongoose.Schema
// const Topic = require('./TopicModels')

const postSchema = new Schema({
    title: {type: String, required: true, minlength: 4, maxlength: 150},
    content: {type: String, required: true, minlength: 4, maxlength: 2000},
    poster: {type: Schema.Types.ObjectId, ref: 'User'},
    created: {type: Date, default: Date.now},
    updated: {type: Date}
});

module.exports = mongoose.model('Post', postSchema)

4.2. Validator

//  src/validators/validator.js

/*
...
*/
exports.PostValidator = function(req, res, next){
    //title
    req.check('title', 'Title is required.').notEmpty();
    req.check('title', 'Title must be between 4 to 150 characters').isLength({min:4, max:150});
    //content
    req.check('content', 'Write a content').notEmpty();
    req.check('content', 'Content must be between 4 to 2000 characters').isLength({min:4, max:2000});

    //check for errors
    const errors = req.validationErrors();
    if(errors){
        const firstError = errors.map(error => error.msg)[0];
        return res.status(400).json({error: firstError});
    }
    next();
}

4.3. Controllers

//  src/controllers/PostControllers.js



const Post = require('../models/PostModels')

exports.listPost = function(req, res){
    const col = 'title content poster created updated'
    Post.find({}, col, (err, posts) => {
        if(err) {return res.json({err})}
        res.json({posts: posts})
    })
}

exports.detailPost = function(req, res){
    Post.findById(req.params.id).populate('poster').exec(function(err, post) {
        if(err) {return res.json({err})}
        res.json({
            title: post.title,
            content: post.content,
            poster: post.poster.username,
            created: post.created,
            updated: post.updated
        })
    })
}

exports.createPost = function(req, res){
    const post = new Post(req.body)
    post.poster = req.session.user._id
    post.save().then(result => {
        res.json({post: result})
    })
}

exports.editPost = function(req, res){
    Post.findById(req.params.id, 'title content', (err, post) => {
        if(err) {return res.json({err})}
        post.title = req.body.title
        post.content = req.body.content
        post.updated = Date.now()
        post.save().then(result => {
            res.json({post: result})
        })
    })
}

exports.deletePost = function(req, res){
    Post.remove({_id: req.params.id}, (err) => {
        if(err) {return res.json({err})}
        res.json({'mess': 'Delete success'})
    })
}

4.4. Routes

/*
...
*/
const {listPost, detailPost, createPost, editPost, deletePost} = require('../controllers/PostControllers')
const {PostValidator, UserValidator} = require('../validators/validator')
/*
...
*/
router.get('/posts', requiresLogin, listPost)
router.get('/post/:id',requiresLogin, detailPost)
router.post('/post/new', requiresLogin, PostValidator, createPost)
router.put('/post/:id/edit', requiresLogin, PostValidator, editPost)
router.delete('/post/:id', requiresLogin, deletePost)

5. End

Như vậy là đã hoàn thành chức năng Authentication và CRUD 1 cách đơn giản. Hi vọng là bài viết này sẽ 1 phần nào đó giúp được các bạn mới bắt đầu học Nodejs như mình.

Nếu thấy hay, hãy upvote, share để được đẹp trai và xinh gái hơn.