-->
ads here

[Tutorial] Xây dựng JSON RESTful API với SailsJS

advertise here
Hello mọi người, sau những bài viết về productivity tại đâyđây, thì mình sẽ trở lại bằng 1 bài viết về technical và cũng là tutorial đầu tiên của mình. Và bài viết này sẽ nói về 1 JS Framework để xây dựng web app là SailsJS.
Bài viết này sẽ hướng dẫn bạn tạo ra 1 JSON API của một ứng dụng todo-list, một cái tutorial rất quen thuộc khi bắt đầu học một kiến thức mới. Chúng ta sẽ lần lượt đi qua các nội dung sau:
  1. Giới thiệu SailsJS
  2. Mô tả ứng dụng, Config, Setup Enviroment
  3. Model và  Controller
  4. TDD
Chúng ta bắt đầu thôi!

Giới thiệu về SailJS

SailsJS là framework MVC phổ biến nhất của NodeJS, được thiết kế mô phỏng theo mô hình MVC trên Ruby on Rails nhưng được bổ sung thêm các yêu cầu của một web app hiện đại như: data-driven APIs with scalable, service-oriented architecture - Trích trang chủ

Một số điểm nổi bật của SailsJS như sau:

  • 100% Javascript: Được xây dựng hoàn toàn bằng JS
  • Có thể kết nối với khá bất kì DB nào kể cả SQL như MySQL, Postgre… hay No-SQL như MongoDB
  • Tự động tạo REST APIs nhờ blueprint mà không cần phải code
  • Có thể kết hợp với nhiều nền thư viện Front-end như: React, Angular...
Ok, vậy là xong phần giới thiệu, giờ chúng ta bắt tay vào phần chính là xây dựng RESTful API cho một ứng dụng to-do list.

Mô tả ứng dụng, Config và Setup Enviroment

Mô tả ứng dụng

Ứng dụng sẽ gồm 4 API với chức năng CRUD cho to-do list bao gồm:
  • Tạo một task mới
  • Xem danh sách các task đã tạo
  • Cập nhật trạng thái của task hay cập nhật nội dung task
  • Xoá một task đã tạo
Ứng dụng sẽ cần một số "nguyên liệu" sau:
  1. Node JS
  2. SailsJS
  3. MongoDB
  4. Testing combo: Mocha, Chai, Chai-HTTP
Nguyên liệu đã đủ, giờ bắt đầu chiến thôi.

Cài đặt SailsJS

Cài đặt SailsJS global bằng npm hay yarn như sau
npm install -g sails  
Hay
yarn add global sails  
Sau khi cài đặt xong, bạn có thể tạo ứng dụng Sails bằng lệnh
  sails new   
Ở đây mình đặt tên project là demo-sails-js
  sails new demo-sails-js  
Sau khi chạy xong sẽ có 1 thư mục tên demo-sails-js được tạo với cấu trúc bên trong như hình sau
├── Gruntfile.js
├── README.md
├── api
├── app.js
├── assets
├── config
├── node_modules
├── package-lock.json
├── package.json
├── tasks
├── views
└── yarn.lock
OK, khởi tạo project đã xong, tiếp theo chúng ta sẽ cấu hình ban đầu và setup các môi trường để code

Config DB và Setup Enviroment

Thông thường sẽ có môi trường là môi trường dev , môi trường test và môi trường production. Trong Sails, môi trường dev sẽ được cấu hình trong thư mục config, còn các môi trường khác sẽ được cấu hình trong thư mục config/env.Sails hỗ trợ rất nhiều cơ sở dữ liệu từ SQL cho đến No-SQL, vì vậy đối với từng DB sẽ có một package adapter tương ứng để Sails connect tới DB. Trong ứng dụng này, mình sử dụng MongoDB nên sẽ phải dùng adapter là sails-mongo :Cài đặt sails-mongo bằng npm hay yarn
  npm install sails-mongo —save  
hay
  yarn add sails-mongo
Sau khi đã có adapter, tiếp theo chung ta sẽ kết nối ứng dụng Sails với DB. Với mỗi môi trường dev và test mình sẽ dùng 2 DB khác nhau với tên lần lượt là:
  • sails-demo dev enviromnent
  • sails-demo-test cho test enviroment
Để setup DB cho từng môi trường ta thực hiện như sau: Với môi trường dev, bạn vào file config/datastores.js và viết đoạn code sau
 // config/datastores.js  
 module.exports.datastore = {    
  default:{     
    adapter: 'sails-mongo',     
    url: 'mongodb://localhost:27017/sails-demo'     
  }    
 };  
Làm tương tự với môi trường test, thêm đoạn code sau vào file config/env/test.js:
 // config/env/test.js  
 module.exports = {  
   datastores: {  
    default:{  
     adapter: 'sails-mongo',  
     url: 'mongodb://localhost:27017/sails-demo-test'  
    }    
  }    
 };   

Config routes

Ứng dụng sẽ gồm 4 API CRUD ứng với các thao tác tạo, đọc, sửa xoá. Với mỗi thao tác ta sẽ có 1 route riêng để ứng dụng giao tiếp với client như sau:
  • POST - /api/task : để tạo 1 task mới
  • GET - /api/tasks: để hiển thị tất cả các task hiện có
  • PUT - /api/task/id: để cập nhật trạng thái hay nội dung của task
  • DELETE - /api/task/id: để xoá 1 task nào đó
Để cấu hình routes, ta vào file config/routes.js và thêm đoạn code này vào:
 // config/routes.js  
 module.exports.routes = {  
  'get /api/tasks': 'TaskController.getTasks',  
  'post /api/task': 'TaskController.createTask',  
  'put /api/task/:id': 'TaskController.updateTask',  
  'delete /api/task/:id': 'TaskController.deleteTask'  
 }  

Với mỗi route, Task Controller sẽ có 1 hàm để xử lý request đến từ route đó, và trả về kết quá tương ứng. Để hiểu rõ hơn Task Controller hoạt động thế nào, chúng ta sẽ chuyển qua tìm hiểu về Action, Controller và Model trong SailsJS

Action, Controller và Model

Trong sails Action là một đối tượng quan trọng, có nhiệm vụ trả lời những request từ client có thể là mobile hay web browser. Mỗi action gắn với 1 route hay URL, do vậy khi client agent request thông qua 1 URL nhất định thì action gắn với URL đó sẽ có trách nhiệm thực thi business logic và trả về response. Trong phạm vi bài viết này, sẽ chỉ xét đến các action trong Controller. Nếu bạn muốn tìm hiểu sâu hơn về Action có thể đọc ở đây .
Tiếp theo là đến Controller. Trong Sails, cách nhanh nhất để viết một ứng dụng Sails đó là tổ chức các action trong 1 file controller - Controller file trong Sails có tên đặt theo dạng Camel Case và kết thúc bằng Controller. Ví dụ: UserController.js.
Trong mỗi file Controller sẽ chứa các action của controller mà mỗi action gắn với 1 route tương ứng như đã nói ở trên.
Để hiểu rõ hơn, chúng sẽ bắt đầu tạo Controller cho ứng dụng của chúng ta bằng cách gõ lệnh sau trong terminal để Sails tự động tạo ra Controller file:
Plain Text>  sails generate controller task


Khi đó, một file TaskController.js sẽ được tự động tạo ra trong thư mục controller có dạng như sau:
/**   
  * TaskController   
  *   
  * @description :: Server-side actions for handling incoming requests.   
  * @help  :: See https://sailsjs.com/docs/concepts/actions   
  */   
  module.exports = {   
  }  

Sau khi tạo xong Controller, ta thêm vào các route để xử lý các request của ứng dụng như sau:
/**   
  * TaskController   
  *   
  * @description :: Server-side actions for handling incoming requests.   
  * @help  :: See https://sailsjs.com/docs/concepts/actions   
  */   
  module.exports = {   
  getTasks: async (req, res) => {},   
  createTask: async (req, res) => {},   
  updateTask: async (req, res) => {},   
  deleteTask: async (req, res) => {},   
  }   

OK, bỏ Controller sang một bên, chúng ta sẽ đi đến phần tiếp theo của ứng dụng đó là Model. Model là đại diện cho một tập dữ liệu có cấu trúc. Mỗi model ứng với 1 table trong SQL hay collection trong No-SQL. Trong Sails, model được định nghĩa trong thư mục model.

Như ta biết, một todo list sẽ gỗm các task, mõi task sẽ có nội dung và trạng thái để biết đã hoàn thành hay chưa. Do vậy Task model của chúng ta sẽ được tạo như sau: Trong thư mục model, tạo một file tên TaskTask.js có nội dung như sau
 // api/models/Task.js   
  module.exports = {   
  attributes: {   
   text: { type: 'text', required: true },   
   status: { type: 'boolen', defaultsTo: false },   
  },   
  }; 

Vậy là mọi thứ chuẩn bị đã xong, chúng ta sẽ qua phần quan trọng nhất, hiện thực ứng dụng và áp dụng TDD vào đó.

TDD

Setup Unit test

Để viết unit test cho Sails, chúng ta cần test runner, ở đây sẽ là mocha, tiếp theo là tới assertion library, mình sẽ dùng chai và chai-http. Để cài đặt combo này ta dùng lệnh sau:
npm install mocha chai chai-http 
hay
yarn add mocha chai chai-http  

Tiếp theo ta sẽ tạo một thư mục chứa các test case unit test. Tạo thư mục test như sausau
├── integration
│   ├── controllers
│   │   └── TaskController.test.js
│   ├── helpers
│   └── models
└── lifecycle.test.js
Trong đó TaskController.test.js là file chứa testcase cho Task Controller. lifecycle.test.js là để setup before và after cho mối lần test. Tìm hiểu thêm tại đây

Tiếp theo sẽ là tạo script để chạy unit test, vào file package.json, thêm dòng sau đây vào mục script:

"test": "NODE_ENV=test node ./node_modules/mocha/bin/mocha test/lifecycle.test.js test/integration/**/*.test.js", 

Từ bây giờ, khi chạy lệnh npm run test thì biến môi trường được set là "test" khi đó mọi thao tác của ứng dụng sẽ chỉ có tác dụng trên moi trường test, chẳng hạn như DB thì sẽ là DB sails-demo-test như ta đã setup ở trên.

Red-green-refactor Cycle

Setup, cấu hình các kiểu đã xong, giờ sẽ tới phần quan trọng nhất đó là implementation. Quy trình sẽ là:
  1. Viết một unit test fail
  2. Viết code sao cho pass được unit test
  3. Refactor code lại
OK, let begin with route GET- /api/tasks
Trong thư mục test/integration/controllers, tạo file TaskController.test.js, file này sẽ chứa unit test cho Task controller của chúng ta:

 // TaskController.test.js  
 /* eslint no-unused-vars: "off" */  
 let chai = require('chai');    
 let sails = require('sails');    
 let chaiHttp = require('chai-http');    
 let should = chai.should();    
 chai.use(chaiHttp);    
 describe('Test Task controller', () => {  }  

Ta sẽ viết test cho API đầu tiên GET /api/tasks. API này sẽ trả về danh sách các task hiện có. Ta thêm đoạn code sau vào file test:

// TaskController.test.js  
 // Some code....  
 describe('GET /api/tasks - Return all tasks', () => {  
   before(done => {  
    Task.destroy({}, (err, results) => {  
     if (err) {  
      return done(err);  
     }  
     return done();  
    });  
   });  
   it('It should return 200 when return all tasks successfully', done => {  
    let firstTask = {  
     text: 'first task'  
    };  
    Task.create(firstTask).exec((err, result) => {  
     if (err) {  
      sails.log(err);  
     } else {  
      chai.request(sails.hooks.http.app)  
       .get('/api/tasks')  
       .end((err, res) => {  
        if (err) {  
         sails.log(err);  
        }  
        res.should.have.status(200);  
        res.body.should.have.property('success');  
        res.body.success.should.be.equal(true);  
        res.body.should.have.property('tasks');  
        res.body.tasks.should.be.an('Array');  
        res.body.tasks.length.should.be.equal(1);  
        done();  
       });  
     }  
    });  
   });  
  });  

Giải thích code:
  • describe(‘GET /api/tasks - Retunr all task’, () => {} : Mô tả nội dung test
  • Hàm before dùng để xoá hết tất cả các document trong DB trước khi chạy test, để đảm bảo dữ liệu sạch khi test
  • it('It should return 200 when return all tasks successfully', done => {}: nội dung test sẽ được thực hiện trong hàm này
  • Test step: 
    • Tạo ra 1 task và lưu xuống DB
    • Gọi API lấy hết danh sách các task đã tạo (ở đây là 1)
    • Assert response trả về
Sau khi viết unit test xong ta chạy lệnh
npm run test
và kết quả nhận được là test fail. Vậy là ta làm xong bước 1. Viết Unit test fail trong Red-Green-Refactor Cycle. Tiếp là bước 2, viết code để pass unit test đó. Trong file TaskController.js ta thêm đoạn code sau

 // TaskController.test.js  
 /* eslint no-unused-vars: "off" */    
 let chai = require('chai');    
 let sails = require('sails');   
 let chaiHttp = require('chai-http');    
 let should = chai.should();    
 chai.use(chaiHttp);    
 describe('Test Task controller', () => {  }

Sau đó bạn chạy lại test

npm run test

Khi đó kết quả sẽ là pass

GET /api/tasks - Return all tasks     
  ✓ It should return 200 when return all tasks successfully (66ms)

Done, vậy là bạn đã có 1 API được unit test và pass. Từ bây giờ, khi bạn refactor đoạn code này, bạn có thể thoải mái, sau đó chạy test đến khi nào test đúng thì đoạn code refactor của bạn đã đảm bảo work và không phát sinh ra lỗi khi refactor - Hoàn thành 1 chu kỳ Test-Code-Refactor
Chúng ta sẽ bước sang tiếp API thứ 2 là tạo một task mới POST /api/task. Các bước cũng tương tự như trên. Đầu tiên API này sẽ trả về {success:true} khi tạo 1 task thành công.
Viết test và chạy thử để fail nào:

// TaskController.test.js  
 describe('POST /api/task - Create new task', () => {  
    it('It should return 200 when create new task successfully', done => {  
    let newTask = {    text: ' Test new task'   };  
    chai.request(sails.hooks.http.app)  
     .post('/api/task')  
     .send(newTask)  
     .end((err, res) => {  
     if (err) {  
      sails.log(err);  
     }  
     res.should.have.status(200);  
     res.body.should.have.property('success');  
     res.body.success.should.be.equal(true);  
     done();  
     });  
    });  
   }

Viết code:

// TaskController.js  
 createTask: async (req, res) => {  
   let taskContent = req.body.text;  
   let newTask = {  
    text: taskContent  
   };  
    Task.create(newTask).exec((err) => {  
     if (err) {  
      return res.serverError({  
       success: false,  
       message: 'Server Error'  
      });  
     }  
     return res.ok({  
      success: true  
     });  
    });  
  },

Và chạy test lại và pass

POST /api/task - Create new task  
    ✓ It should return 200 when create new task successfully (88ms)

Tiếp theo ta để ý thấy, nếu thiếu nội dung task thì API sẽ trả về lỗi 400 Bad Request với message: ‘Missing content of task’
Viết test tiếp theo cho case này:

// TaskController.test.js  
 describe('POST /api/task - Create new task', () => {  
   it('It should return 200 when create new task successfully', done => {  
    let newTask = {  
     text: ' Test new task'  
    };  
    chai.request(sails.hooks.http.app)  
     .post('/api/task')  
     .send(newTask)  
     .end((err, res) => {  
      if (err) {  
       sails.log(err);  
      }  
      res.should.have.status(200);  
      res.body.should.have.property('success');  
      res.body.success.should.be.equal(true);  
      done();  
     });  
   });  
   it('It should return 400 when missing task content in request body', done => {  
    let newTask = {};  
    chai.request(sails.hooks.http.app)  
     .post('/api/task')  
     .send(newTask)  
     .end((err, res) => {  
      if (err) {  
       sails.log(err);  
      }  
      res.should.have.status(400);  
      res.body.should.have.property('success');  
      res.body.success.should.be.equal(false);  
      res.body.should.have.property('message');  
      res.body.message.should.be.equal('Missing content of task');  
      done();  
     });  
   });  
  });

Chạy test và fail cho case 400 vì ta chưa có code xử lý nó. Viết code nào:

// TaskController.js  
 createTask: async (req, res) => {  
   let taskContent = req.body.text;  
   let newTask = {  
    text: taskContent  
   };  
   if (!taskContent) {  
    return res.badRequest({  
     success: false,  
     message: 'Missing content of task'  
    });  
   } else {  
    Task.create(newTask).exec((err) => {  
     if (err) {  
      return res.serverError({  
       success: false,  
       message: 'Server Error'  
      });  
     }  
     return res.ok({  
      success: true  
     });  
    });  
   }  
  },

Chạy test lại và đây là kết quả

POST /api/task - Create new task  
    ✓ It should return 200 when create new task successfully (88ms)  
   verbo: Sending 400 ("Bad Request") response:  { success: false, message: 'Missing content of task' }  
    ✓ It should return 400 when missing task content in request body

Done, vậy là chúng ta đã hoàn thành 2 API cho ứng dụng này. Còn lại 2 API là PUT và DELETE bạn sẽ làm tương tự nhé. Mình sẽ để source trên GitHub để mọi người tiện tham khảo

Kết

Vậy là chúng ta đã thực hiện xong 1 RESTful API đơn giản với SailsJS. Thực tế, khi bạn tìm trên Google về RESTful API SailsJS thì cũng sẽ thấy rất nhiều tutorial khác nhưng mà đều là bản cũ, khác hẳn với document bây giờ trên trang chủ, mình cũng gặp trường hợp này và quyết định viết bài này để cho ai cần tìm hiểu về Sails có thể tham khảo. Đây là bài viết kiểu tutorial đầu tiên của mình, chắc chắn sẽ có những chỗ thiếu xót, mong mọi người góp ý để những bài viết sau của mình tốt hơn. Cảm ơn mọi người đã dành thời gian đọc!

Reference

Advertisement
COMMENTS ()