[Nodejs] - Todo REST API(Part 1)

RESTful APIs

REST là viết tắt của Representational State Transfer. Giải thích đơn giản, REST là một loạt hướng dẫn và dạng cấu trúc dùng cho việc chuyển đổi dữ liệu. Thông thường, REST hay được dùng cho ứng dụng web, nhưng cũng có thể làm việc được với dữ liệu phần mềm.

API viết tắt của Application Programming Interface , tạm dịch là Giao diện lập trình ứng dụng, nghĩa là một phương thức để tạo các giao tiếp giữa các ứng dụng phần mềm khác

Nhìn chung, RESTful API là những API đi theo cấu trúc REST

Người dùng API có thể build một script kết nối đến một API server, rồi dữ liệu cần thiết sẽ chuyển sang HTTP. Lập trình viên khi đó có thể hiển thị dữ liệu lên website mà không cần đến truy cập cá nhân vào server, có bốn phương thức cơ bản dùng để truy cập RESTful API:

  1. GET để truy vấn object
  2. POST để tạo object mới
  3. PUT để sửa đổi hoặc thay thế một object
  4. DELETE để loại bỏ một object

Mỗi phương thức trên phải được API call thông qua để gửi chỉ thị cho server phải làm gì.

Đại đa số web API chỉ cho phép GET request lấy dữ liệu khỏi một server. Hoặc web API phải thực hiện Authencation nếu không người sử dụng có thể sử dụng các lệnh khá “nguy hiểm” như PUT hay DELETE.

Setup project

Trước khi xây dựng ứng dụng cần cài đặt và thiết lập như sau: Bước 1: Cài đặt postman để có thể tạo các request lên server một cách dễ dàng (download tool tại đây https://www.getpostman.com/) Bước 2: Sau khi cài đặt xong, tạo một project node js theo các bước sau(nếu chưa cài đặt có thể tham khảo tại đây

npm init

npm  install express --save
// express framwork là thư viện chính dùng để tạo API service của ứng dụng xem thêm tại:
// https://expressjs.com/

git init

touch .gitignore 
// tạo file gitignore để loại bỏ các thư việc node module khi đẩy lên repo.
// Trong file .gitignore ta thêm vào các thư mục như sau: node_modules/

Bước 3: Tạo file serverjs với nội dung như sau:

// import thư viện express
var express = require('express');
var app = express();
// khai báo cổng chạy dịch vụ
var PORT = process.env.PORT || 3000;

// "To do API Root" sẽ được trả về khi thực hiện get request trên trang home page của ứng dụng  
 app.get('/', function(req, res) {
  res.send('To do API Root')
 });

app.listen(PORT, function() {
  console.log('Express listening on port' + PORT + '!');
});

Sau khi thực hiện xong các bước trên, trong comman-line chạy node serverjs và chạy thử trang http://localhost:3000/ để xem kết quả

Getting All Todos

Do ứng dụng ban đầu sẽ được xây dựng với dữ liệu đơn giản có sắn do đó, ta sẽ tạo sẵn model trên server như sau:

var todos =[{
 id: 1,
 description: 'Build a simple API - nodejs',
 completed: false
}, {
 id: 2,
 description: 'Go to T-beer - team building',
 completed: false
}, {
 id: 3,
 description: 'Feed the dog ',
 completed: true
}];

Sau đó thêm vào sau app.get('/', function(req, res) {...});

 // GET /todos
app.get('/todos'. function(req, res) {
 res.json(todos);
});

Để kiểm tra API đã có thể sử dụng được chưa, khởi động lại server sử dụng post man và nhập vào đường dẫn: http://localhost:3000/todos với phương thức GET, kết quả như sau:

Get Todo By Id

Để xây dựng được phương thức get object by Id trên express framwork cần hiểu cơ chế route parameters . Các biến được gửi lên cùng với đường dẫn URL nằm trong object req.params{}. Ví dụ:

Route path: /users/:userId/books/:bookId
Request URL: http://localhost:3000/users/34/books/8989
req.params: { "userId": "34", "bookId": "8989" }

Ngoài ra trong đường dẫn có thể sử dụng các dấu - hoặc . để phân biệt giữa các biến được truyền lên tùy từng mục đích cụ thể ví dụ

Route path: /flights/:from-:to
Request URL: http://localhost:3000/flights/LAX-SFO
req.params: { "from": "LAX", "to": "SFO" }

Route path: /plantae/:genus.:species
Request URL: http://localhost:3000/plantae/Prunus.persica
req.params: { "genus": "Prunus", "species": "persica" }

Để thực hiện get object todo by id được truyền lên, ý tưởng thực hiện như sau: duyệt tất cả object trong todos kiểm tra id truyền lên trùng nếu tìm thấy thì trả kết quả về, nếu không thì thông báo lỗi, trả về status 404 như sau:

app.get('/todos/:id', function(req, res) {
  // params được gửi thuộc kiểu string do đó phải convert params về kiểu integer 
  var todoId = parseInt(req.params.id, 10);
  var matchedTodo;
  // duyệt từng phần tử trong todos
  todos.forEach(function (todo) {
    if (todoId == todo.id) {
      matchedTodo = todo;
    }
  });
  // nếu tồn tại kết quả thì trả về dưới dạng json nếu không trả về status 404
  if (matchedTodo) {
    res.json(matchedTodo);
  } else {
    res.status(404).send();
  }
});

Kết quả sau khi thực hiện với cả 2 trường hợp(khởi động lại server trước khi thực hiện test): Trường hợp tìm thấy object: Trường hợp không tìm thấy:

Creating New Todos

Để có thể cài đặt cho phương thức tạo mới một todo object, trước tiên cần cài đặt module body-parser. Body-parser sẽ trích xuất toàn bộ nội dung trong body của request gửi lên và đưa vào đối tượng req.body. Để cài đặt module trong terminal gõ lệnh sau:

npm install body-parser --save

Sau đó trong server.js thêm đoạn sau:

var bodyParser = require('body-parser');

var todoNextId = 4;

app.use(bodyParser.json())

Tiếp tục implement cho route với phương thức POST để thêm mới một object todo, ý tưởng thực hiện như sau: thêm object mới nhận được vào mảng todos trong server trước đó thêm vào object mới nhận được trường field id.

app.post('/todos', function(req, res) {
  var body = req.body;

  body.id = todoNextId++;

  todos.push(body);

  res.json(body);
});

Để tạo request có phương thức POST thêm mới một object ta thực hiện config như sau: trong Body chọn raw kiểu JSON(application/json), nội dung gửi lên {"description": "", "completed": false}. Khởi động lại server và chạy thử để thêm mới 1 đối tượng sau đó chạy lại http://localhost:3000/todos với phương thức GET để kiểm tra kết quả đã được thêm mới chưa.

[
    {
        "id": 1,
        "description": "Build a simple API - nodejs",
        "completed": false
    },
    {
        "id": 2,
        "description": "Go to T-beer - team building",
        "completed": false
    },
    {
        "id": 3,
        "description": "Feed the dog ",
        "completed": true
    },
    {
        "description": "This is a test",
        "completed": false,
        "id": 4
    }
]

Deleting Todos By Id

Trước khi implement phương thức delete, để code rõ ràng hơn ta cần refactor lại code bằng cách cài đặt module underscore(http://underscorejs.org). Đây là một module được sử dụng rất nhiều bởi nó hỗ trợ các phương thức xử lý object trong javascript

npm install underscore --save

Trong file server.js cần khai báo và refactor code ở hai phần: thứ nhất phần tìm kiếm object todo, phần thứ 2 validate và permit params khi thêm mới một todo object.

var _ = require('underscore');

// refactor find object in todos 
app.get('/todos/:id', function(req, res) {
  var todoId = parseInt(req.params.id, 10);
 var matchedTodo = _.findWhere(todos, {id: todoId});
 ...
});

// validate and permit params when create new object
app.post('/todos', function(req, res) {
  var body = _.pick(req.body, 'description', 'completed');   //never trust parameters from the scary internet

  if (!_.isBoolean(body.completed) || !_.isString(body.description) ||
    body.description.trim().length == 0) {
    return res.status(400).send();
  }
  ...
});

Để tạo phương thức delete một todo object, ý tưởng thực hiện như sau: tìm object bởi id được gửi lên, nếu không có trả về status 404 nếu có thì thực hiện xóa và trả kết quả vừa xóa. Bằng cách sử dụng các phương thức trong module underscore, có thể implement phương thức delete theo ý tưởng đưa ra ở trên.

app.delete('/todos/:id', function(req, res) {
  var todoId = parseInt(req.params.id, 10);
  var matchedTodo = _.findWhere(todos, {id: todoId});

  if(!matchedTodo) {
    res.status(404).json({"error": "no todo found with that id"});
  } else {
    todos = _.without(todos, matchedTodo);
    res.json(matchedTodo);
  }
});

Restart server and check result again.

Updating Todos

Tương tự với ý tưởng của các phương thức được cài đặt trên: ban đầu id được gửi lên ta sẽ tìm kiếm todo object tương ứng, thực hiện validate các trường được gửi lên nếu hợp lệ chuyển sang thực hiện cập nhật dữ liệu.

Trong quá trình kiểm tra ta sẽ có các case cần kiểm tra như sau: Case 1: khi dữ liệu truyền lên thỏa mãn điều kiện => lưu dữ liệu thỏa mãn vào biến riêng Case 2: có dữ liệu truyền lên nhưng không thỏa mãn điều kiện => return false và thông báo lỗi Case 3: không có dữ liệu truyền lên => không có dữ liệu nên trả về rỗng

// PUT /todos/:id
app.put('/todos/:id', function(req, res) {
  var body = _.pick(req.body, 'description', 'completed');
  var validAttributes = {}

  var todoId = parseInt(req.params.id, 10);
  var matchedTodo = _.findWhere(todos, {id: todoId});

  if (!matchedTodo) {
    return res.status(404).json();
  }

  if (body.hasOwnProperty('completed') && _.isBoolean(body.completed)) {
    validAttributes.completed = body.completed;
  } else if (body.hasOwnProperty('completed')){
    return res.status(404).json();
  }

  if (body.hasOwnProperty('description') && _.isString(body.description) &&
    body.description.trim().length > 0) {
    validAttributes.description = body.description;
  } else if (body.hasOwnProperty('description')) {
    return res.status(404).json();
  }

  _.extend(matchedTodo, validAttributes);
  res.json(matchedTodo);

});

Kết quả sau khi cài đặt :

Filtering By Todo Completed Status

Ý tưởng thực hiện: sử dụng phương thức GET để load dữ liệu về, gửi kèm các tham số để server truy vấn dữ liệu dựa trên các thâm số được truyền nên. Cấu trúc như sau: {{apiURL}}/todos?completed=true&key=value, sau khi nhận các params truyền lên server sẽ kiểm tra có tồn tại các key filter hay không nếu có thì sẽ thực hiện filter.

Ở phần trước trong module UnderScore ta sử dụng findWhere để tìm kiếm object dựa trên id tuy nhiên phương thức này chỉ chả về kết quả đầu tiên tìm được, do đó ta sẽ phải tìm phương thức khác rất may UnderScore hỗ trợ việc filter bằng phương thức _.where(list, properties)

Ta sửa 1 chút phần GET /todos để phù hợp với filter như sau:

// GET /todos
app.get('/todos', function(req, res) {
  var queryParams = req.query;
  var filteredTodos = todos;

  if (queryParams.hasOwnProperty('completed') && queryParams.completed == 'true') {
    filteredTodos = _.where(filteredTodos, {
      completed: true
    });
  } else if (queryParams.hasOwnProperty('completed') && queryParams.completed == 'false') {
    filteredTodos = _.where(filteredTodos, {
      completed: false
    });
  }
  res.json(filteredTodos);
});

Kết quả sau khi chạy server:

Searching By Todo Description

Tương tự việc filter ta sẽ gửi params lên để server thực hiện tìm kiếm các object todo dựa trên description. Tuy nhiên không giống trong mysql ta có thể sử dụng câu truy vấn LIKE để tìm kiềm gần đúng. Do vậy ở đây ta sử dụng cách sau để tìm kiếm gần đúng như sau: trong javascript có hàm indexOf trả về kết quả là vị trí của từ cần so sánh trong câu mẫu ban đầu. Thử bật f12 và trong console chạy đoan code sau 'Hello world'.indexOf('world') kết quả trả về là 6 trong trường hợp không trùng thì kết quả trả về là -1.

Tiếp theo trong module Underscore cũng hỗ trợ ta phương thức filter cho phép truyền callback để xử lý các item trong array todos . Giả sử params tìm kiếm có key là search ta sẽ thực hiện như sau:

app.get('/todos', function(req, res) {
  var queryParams = req.query;
  var filteredTodos = todos;

 ...

  if (queryParams.hasOwnProperty('search') && queryParams.search.length > 0) {
    filteredTodos = _.filter(filteredTodos, function(item) {
      return item.description.indexOf(queryParams.search) > -1
    });
  }

  res.json(filteredTodos);
});

Khởi động lại servervà thực hiện GET todos với params search như sau:

Deploy

Làm theo các bước “Introduction”, “Set up” tại trang https://devcenter.heroku.com/articles/getting-started-with-nodejs#introduction để đăng ký tài khoản, cài đặt Heroku CLI và đăng nhập.

Config lại code để có thể chạy được trên môi trường production, trong file package.jsonphần scripts sửa lại như sau:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
},

Tiếp theo ta sử dụng git để quản lý phiên bản, thực hiện các câu lệnh sau đây:

git add .
git commit -m "deploy commit"

Chúng ta tạo ứng dụng Heroku bằng cách gõ lệnh heroku create trên terminal và sẽ nhận được kết quả như dưới đây:

Creating app... done, ⬢ warm-meadow-18486
https://warm-meadow-18486.herokuapp.com/ | https://git.heroku.com/warm-meadow-18486.git

Trong trường hợp này warm-meadow-18486 là tên ứng dụng của bạn. Tiếp theo chúng ta đẩy mã nguồn lên Heroku bằng cách gõ lệnh git push heroku master. Kết quả như sau;

remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Procfile declares types     -> (none)
remote:        Default types for buildpack -> web
remote: 
remote: -----> Compressing...
remote:        Done: 16.2M
remote: -----> Launching...
remote:        Released v4
remote:        https://warm-meadow-18486.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/warm-meadow-18486.git
   1d66b67..758fbaa  develop -> master

Cuối cùng gõ lệnh heroku open để mở ứng dụng bạn vừa deploy. Bạn có thể tham khảo ở trang sau: https://warm-meadow-18486.herokuapp.com

Conclusion

Ứng dụng còn dừng lại ở mức độ đơn giản. Trong những ngày tới mình sẽ viết tiếp một số phần để cải thiện ứng dụng ví dụ: kết nối node vs mongoDB, thêm các chức năng filter, search. Cảm ơn các bạn đã theo dõi