ในเนื้อหาตอนที่แล้ว เราได้จำลองการสร้าง และใช้งาน
RESTful API ด้วย Express แล้วยังไม่ได้พูดถึงในเรื่องการ
ตรวจสอบความถูกต้องของข้อมูล ที่ถูกส่งเข้ามาในขั้นตอน
การเพิ่มข้อมูลหรือ POST Request และขั้นตอนการแก้ไขหรืออัพเดทข้อมูล
ด้วย PUT Request ทบทวนเนื้อหาตอนที่แล้วได้ที่ http://niik.in/913
ในการตรวจสอบความถูกต้องของข้อมูล หรือที่เรียกว่า Data Validation
สำหรับใน Express เราจะใช้งาน Joi ซึ่งเป็น Package Module ที่มีรูปแบบการใช้งานที่ง่าย
ดูรายละเอียดเพิ่มเติมได้ที่ Joi - Object schema validation
การติดตั้ง Joi
npm install --save @hapi/joi
การใช้งาน Joi
Joi มีรูปแบบการใช้งานที่ค่อนข้างง่าย มีด้วยกัน 2 ขั้นตอนหลักๆ ดังนี้ คือ
1. สร้างชุดรูปแบบการตรวจสอบข้อมูล ที่เรียกว่า schema
2. ตรวจสอบความถูกต้องของข้อมูล กับ schema ด้วยคำสั่ง validate()
การกำหนด Schema
เราสามารถกำหนด schema ได้ 2 แบบคือ
แบบ joi type
const schema = Joi.string().min(10);
แบบ JavaScript Object ที่กำหนด แต่ละ key เป็น joi type
const schema = Joi.object().keys({ a: Joi.string(), b: Joi.number() })
การ Validate ข้อมูล
เราสามารถตรวจสอบความถูกต้องของข้อมูลด้วยคำสั่ง validate() ด้วยรูปแบบดังนี้
const {error, value} = Joi.validate({ a: 'a string' }, schema)
หรือใช้งานรูปแบบฟังก์ชั่น callback
Joi.validate({ a: 'a string' }, schema, function (error, value) { // if(error){ } // ค่า value คือ Ojbect ข้อมูลทีตรวจสอบ หรือก็คือ { a: 'a string' } }) // arrow function Joi.validate({ a: 'a string' }, schema, (error, value) => { // if(error){ } // ค่า value คือ Ojbect ข้อมูลทีตรวจสอบ หรือก็คือ { a: 'a string' } })
ข้อมูลหรือชุดของข้อมูลผ่านการตรวจสอบหรือ valid เมื่อค่า error เท่ากับ null
เราสามารถกำหนดเงื่อนไขการทำงานกรณี error หรือ กรณี valid เช่น
if(error){ } // กรณีมี error if(!error){ } // กรณี valid
ในกรณีเกิด error ขึ้น จะมี Error object ที่มีรายละเอียดข้อมูลต่างๆ เกี่ยวกับ error ที่เกิดขึ้น
ซึ่งเราสามารถนำไปใช้งานต่อได้
การกำหนดรูปแบบใน schema
ก่อนไปดูการใช้งาน Joi กับโปรเจ็ค Express ของเรา มาทำความเข้าใจเล็กน้อยกับรูปแบบที่ใช้ในการ
กำหนดในค่า schema รูปแบบที่ใช้งานคือ
{ [ชื่อ ฟิลด์ข้อมูล ในที่นี้เรียก key]: [การกำหนดรูปแบบเงื่อนไข] }
โดยจะกำหนดค่าเหล่านี้ไว้ใน Joi.object().keys() ตัวอย่างเช่น เราส่งฟิลด์ข้อมูลที่มี "name" กับ "email" เข้ามา
และมีเงื่อนไขการตรวจสอบเป็นดังนี้คือ
"name" => เป็นข้อความ ตัวอักษร 3 - 30 ตัว และเป็นข้อมูลจำเป็นที่ต้องมี ไ่ม่เป็นค่าว่าง "email" => เป็นข้อความในรูปแบบ email และมีส่วนโดเมน อย่างน้อยสองส่วน เชน example.com
ก็จะได้รูปแบบ การกำหนดแต่ละ key เป็นดังนี้
const schema = Joi.object().keys({ name: Joi.string().min(3).max(30).required(), email: Joi.string().email({ minDomainSegments: 2 }) })
การกำหนดรูปแบบเงื่อนไขต่อๆ กันแบบต่อเนื่องเป็นคุณสมบัติของ Joi ที่ทำให้เราสร้างเงือนไขด้วยรูปแบบ
ทีง่าย สื่อความหมายได้เข้าใจง่าย อย่างเช่น
Joi.string() - เป็นข้อมูลประเภท String ข้อความ .min(3) - ความยาวตัวอักษรอย่างน้อย 3 ตัว .max(30) - ความยาวตัวอักษรสูงสุดไม่เกิน 30 ตัว .required() - เป็นข้อมูลที่จำเป็นต้องระบุ
สามารถดูรูปแบบการใช้งานเพิ่มเติมได้ที่ API Reference
ใช้งาน Joi ร่วมกับ Express
เมื่อเรารู้จักรูปแบบ และวิธีการใช้งาน Joi ในการตรวจสอบความถูกต้องของข้อมูล เบื้องต้นไปแล้ว
ต่อไป เราจะลองนำรูปแบบการใช้งานข้างต้น มาใช้งานร่วมกับ RESTful API ในกรณี POST และ PUT
ข้อมูล จะขอใช้ไฟล์ users.js ซึ่งเป็น router ของ users api จากตอนที่แล้ว ตัดส่วน comment ต่างๆ ออก
และเพิ่มส่วนของการตรวจสอบข้อมูล โดยใช้ Joi จะได้เป็นดังนี้
ไฟล์ users.js [routes/users.js]
const express = require('express') const router = express.Router() const users = require('../mock-users') const Joi = require('@hapi/joi') router.route('/users?') .get((req, res, next) => { const result = { "status": 200, "data": users } return res.json(result) }) .post((req, res, next) => { // กำหนดชุดรูปแบบ schema const schema = Joi.object().keys({ name: Joi.string().min(3).max(30).required(), email: Joi.string().email({ minDomainSegments: 2 }) }) // ทำการตรวจสอบความถูกต้องของข้อมูล req.body ที่ส่งมา Joi.validate(req.body, schema, function (error, value) { // กรณีเกิด error ข้อมูลไม่ผ่านการตรวจสอบ if(error) return res.status(400).json({ "status": 400, "message": "Bad request" }) }) let user = { "id": users.length + 1, "name": req.body.name, "email": req.body.email } users.push(user) const result = { "status": 200, "data": users } return res.json(result) }) router.route('/user/:id') .all((req, res, next) => { let user = users.find((user) => user.id === parseInt(req.params.id)) if (!user) return res.status(400).json({ "status": 400, "message": "Not found user with the given ID" }) res.user = user next() }) .get((req, res, next) => { const result = { "status": 200, "data": res.user } return res.json(result) }) .put((req, res, next) => { // กำหนดชุดรูปแบบ schema const schema = Joi.object().keys({ name: Joi.string().min(3).max(30).required(), email: Joi.string().email({ minDomainSegments: 2 }) }) // ทำการตรวจสอบความถูกต้องของข้อมูล req.body ที่ส่งมา Joi.validate(req.body, schema, function (error, value) { // กรณีเกิด error ข้อมูลไม่ผ่านการตรวจสอบ if(error) return res.status(400).json({ "status": 400, "message": "Bad request" }) }) let user = { "id": res.user.id, "name": req.body.name, "email": req.body.email } const result = { "status": 200, "data": user } return res.json(result) }) .delete((req, res, next) => { let user = users.filter((user) => user.id !== parseInt(req.params.id)) const result = { "status": 200, "data": user } return res.json(result) }) module.exports = router
จะเห็นว่า เริ่มต้นด้วยการเรียกใช้งาน Joi module ด้านบน ต่อมาเรากำหนด schema ที่จะใช้ในการกำหนด
รูปแบบความถูกต้องของข้อมูล และสุดท้ายเราทำการตรวจสอบความถูกต้องด้วยคำสั่ง validate() ซึ่งในโค้ด
เรากำหนดไว้เหมือนกันทั้งสองส่วนคือส่วนของ POST กรณีเพิ่มข้อมูล และ PUT กรณีอัพเดทข้อมูล สำหรับสถานะ
กรณีเกิด error ในเบื้องต้น เรากำหนดข้อความเป็น "Bad request" ไปก่อน
ต่อไปเราลองทดสอบ ส่งข้อมูลที่มีรูปแบบไม่ถูกต้อง เข้าไปใน RESTful API ดู เป็นดังนี้
โดยเรากรอกในส่วนของรูปแบบ email ให้เป็นรูปแบบที่ผิดจากเงื่อนไข ก็จะได้ผลลัพธ์ดังรูป
การตรวจสอบข้อมูลโดยใช้ Joi ทำให้เราจัดการกับความถูกต้องของข้อมูล ก่อนจะถูกนำไปใช้งานต่อได้อย่างง่ายดาย
อย่างไรก็ตาม รูปแบบการใช้งาน โดยแทรกไปในไฟล์ api ข้างต้น หากจำเป็นต้องแก้ไข หรือโค้ดมีจำนวนบรรทัดหรือคำสั่ง
มากๆ ก็อาจจะไม่สะดวกมากนัก เราจะประยุกต์โดยสร้างเป็นไฟล์แยก และเรียกใช้งานในลักษณะ middleware ฟังก์ชั่น
ให้เราสร้างโฟลเดอร์ชื่อ validator สำหรับเก็บไฟล์ที่กำหนดรูปแบบ schema และฟังก์ชั่นการตรวจสอบข้อมูล
จากนั้นสร้างไฟล์ users.js ไว้ในด้าน
ไฟล์ users.js [validator/users.js]
const Joi = require('@hapi/joi') const validation = (schema) =>{ return ((req, res, next) => { // ทำการตรวจสอบความถูกต้องของข้อมูล req.body ที่ส่งมา Joi.validate(req.body, schema, function (error, value) { // กรณีเกิด error ข้อมูลไม่ผ่านการตรวจสอบ if(error) return res.status(400).json({ "status": 400, "message": error.details[0].message }) if(!error) next() }) }) } // กำหนดชุดรูปแบบ schema const schema = Joi.object().keys({ name: Joi.string().min(3).max(30).required(), email: Joi.string().email({ minDomainSegments: 2 }) }) module.exports = { validation, schema }
ในไฟล์ข้างต้น เราทำการสร้างฟังก์ชั่นชื่อว่า validation() มี parameter 1 ตัวคือ schema
และสร้าง JavaScript Object ในตัวแปรชื่อ schema
ฟังก์ชั่น validation() เมื่อเรียกใช้งาน จะเป็นการ return middleware ฟังก์ชั่นออกมา นั่นก็คือฟังก์ชั่น
validation() ทำการสร้างฟังก์ชั่น middleware นั่นเอง
ใน middleware ฟังก์ชั่น เราก็ทำการตรวจสอบข้อมูล req.body ด้วย schema ที่ส่งเข้ามาในฟังก์ชั่น
และทำการ return status 400 กรณีเกิด error ขึ้น ในที่นี้ เราประยุกต์ให้ข้อความที่แสดง เป็นข้อความที่ได้
จาก Joi กรณีเกิด error ซึ่งมีอยู่ในค่า error object
แต่ถ้าไม่เกิด error ขึ้น ก็ให้ทำคำสั่ง next ไปทำงานต่อใน middleware ฟังก์ชั่นถัดไป
หลังจากเราสร้างฟังก์ชั่นตรวจสอบข้อมูล แล้ว ก็ทำการ export เป็น object ออกไปสำหรับใช้งาน
มาที่ไฟล์ users api ของเรา เดิมที เราใช้งานการตรวจสอบความถูกต้องข้อมูลโดยกำหนดในไฟล์ router
ที่ชื่อ users.js ซึ่งเป็น users api เราจะเปลี่ยนมาเป็นเรียกใช้งานจากอีก module แทน จะได้เป็นดังนี้
ไฟล์ users.js [routes/users.js]
const express = require('express') const router = express.Router() const users = require('../mock-users') const { validation, schema } = require('../validator/users') router.route('/users?') .get((req, res, next) => { const result = { "status": 200, "data": users } return res.json(result) }) .post(validation(schema),(req, res, next) => { let user = { "id": users.length + 1, "name": req.body.name, "email": req.body.email } users.push(user) const result = { "status": 200, "data": users } return res.json(result) }) router.route('/user/:id') .all((req, res, next) => { let user = users.find((user) => user.id === parseInt(req.params.id)) if (!user) return res.status(400).json({ "status": 400, "message": "Not found user with the given ID" }) res.user = user next() }) .get((req, res, next) => { const result = { "status": 200, "data": res.user } return res.json(result) }) .put(validation(schema),(req, res, next) => { let user = { "id": res.user.id, "name": req.body.name, "email": req.body.email } const result = { "status": 200, "data": user } return res.json(result) }) .delete((req, res, next) => { let user = users.filter((user) => user.id !== parseInt(req.params.id)) const result = { "status": 200, "data": user } return res.json(result) }) module.exports = router
เราทำการ เรียกใช้งานฟังก์ชั่น valiation() และใช้งาน schema จาก validator module ด้วยคำสั่ง
const { validation, schema } = require('../validator/users')
รูปแบบการกำหนดตัวแปรในลักษณะข้างต้น เราเรียกว่า destructuring assignment หรือคือการแยก
เอาค่า value ของ array หรือ เอา property ของ object มากำหนดเป็นตัวแปรแยกอย่างชัดเจน
ในกรณีข้างต้น เราเอา property ของ object ที่ export มาจาก validator module มากำหนดเป็นตัวแปร
instance ของข้อมูลนั้นๆ ดังนั้น เราก็จะได้ validation() เป็นฟังก์ชั่น และ schema เป็นชุดรูปแบบการตรวจสอบ
เมื่อเรารู้อยู่แล้วว่า validation() เป็นฟังก์ชั่นที่สร้าง middleware ฟังก์ชั่นอีกที นั่นก็แสดงว่า เมื่อเราเรียกใช้งาน
validation() ฟังก์ชั่น ก็คือเราใช้งาน middleware นั้นเอง ฉะนั้น เราก็สามารถแทรก middleware ฟังก์ชั้่น
ที่ทำหน้าที่ในการตรวจสอบความถูกต้องของข้อมูลนี้ เข้าไปใน POST และ PUT request ได้ ตามรูปแบบ
.post(validation(schema),(req, res, next) => { })
ทำให้เข้าใจง่ายๆ รูปแบบข้างต้นก็คือ การกำหนด middleware ฟังก์ชั่นแบบซ้อนกันหรือที่เรียกว่า stack
มาจากรูปแบบ
.post((req, res, next) => { next() },(req, res, next) => { })
การทำงานก็คือ ทำงานส่วนของ middleware ฟังก์ชั่นที่กำหนดด้านหน้าก่อน แล้วทำตัวถัดไป
ทดสอบการทำงาน
เรามาลองทดสอบการทำงานอีกครั้ง แบบ กรอกชื่อผิดรูปแบบ และกรอกอีเมลผิดรูปแบบ
จะเห็นว่า ข้อความที่แสดงในกรณี error เป็นข้อความที่ได้มาจาก error object ของ Joi
การแยกการตรวจสอบข้อมูลข้างต้น จะทำให้เราสามารถแก้ไข การจัดการเกี่ยวกับการตรวจสอบข้อมูล
ในภายหลังได้ง่ายและสะดวกขึ้น โดยไม่ต้องมายุ่งเกี่ยวกับไฟล์ในส่วนของ api