Tutorial RESTful API Fastify JS + MySQL + Token Auth — Part 1

Simple Auth, dan CRUD dengan framework Node JS yang cepat bernama Fastify.

Hudya
10 min readDec 25, 2018

Hello Devlopah! Pada kali ini gue akan berbagi insight mengenai cara ngebuat RESTful API dengan Fastify JS. Mungkin ada yang bertanya-tanya, Fastify JS tuh apasih? Oke lemme tell ya.

Fastify JS adalah salah satu web framework Node JS yang berfokus kepada kecepatan dan efisiensi, nah meskipun disini disebut web framework bukan berarti ngga reliable untuk dipake sebagai RESTful API. Buktinya udah banyak comparable antara Fastify vs Koa2 vs Express vs Restify vs Hapi.

Nah kalo ada yang nanya apasih kelebihan dari Fastify JS dibanding framework lainnya? saya hanya akan nulis beberapa poinnya aja ya.

  1. Performa benchmark lebih cepat. Berdasarkan artikel dibawah dari raygun.com, Fastify JS lebih unggul daripada Koa2 dan Express. Tapi performanya tetap lebih rendah daripada RAW Node (Node JS tanpa framework).
  2. Asynchronus function. Fungsi bawaannya menggunakan programming asynchronus dimana tiap fungsi yang dibuat otomatis menggunakan async sebagai fungsi dasar.

Berikut saya lampirkan komparasi antara beberapa framework Node JS.

Oke back to topic, setelah saya membagikan beberapa artikel komparasi. Nah sekarang silahkan tentukan apakah agan ingin mencoba atau hanya membaca saja. Kedua opsi ini tidak ada yang salah, membaca berarti menambah wawasan, mencoba berarti menambah pengalaman, so let’s try.

Nah di tutorial kali ini gue juga akan menggunakan Token Authentication yang akan gue buat sendiri. Jadi gue disini ngga akan pake JWT, OAuth, Passport dan berbagai library yang mungkin akan kalian sodorkan ke gue untuk gue pake. Kenapa seperti ini? karena iseng aja gue pengen nyoba rasanya buat sendiri hehehe *alasan gaje 😂😂😂😂.

Nah di tutorial kali ini juga gue akan membuat fungsi asynchronous dalam mengecek setiap token, sehingga tidak ada proses callback hell nantinya.
Ok let’s go!

Pertama kita buat dulu database, kalo saya diberi nama fastify, kalian sih terserah. Kedua silahkan copy file SQL dibawah ini.

-- MySQL dump 10.13  Distrib 5.7.23, for Linux (x86_64)
--
-- Host: 192.168.10.10 Database: fastify
-- ------------------------------------------------------
-- Server version 5.7.22-0ubuntu18.04.1

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `authentication`
--

DROP TABLE IF EXISTS `authentication`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `authentication` (
`id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` int(10) unsigned NOT NULL,
`secret_id` int(10) unsigned NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
UNIQUE KEY `authentication_id_unique` (`id`),
KEY `authentication_secret_id_foreign` (`secret_id`),
KEY `authentication_user_id_foreign` (`user_id`),
CONSTRAINT `authentication_secret_id_foreign` FOREIGN KEY (`secret_id`) REFERENCES `secret` (`id`),
CONSTRAINT `authentication_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `authentication`
--

LOCK TABLES `authentication` WRITE;
/*!40000 ALTER TABLE `authentication` DISABLE KEYS */;
/*!40000 ALTER TABLE `authentication` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `secret`
--

DROP TABLE IF EXISTS `secret`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `secret` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`secret` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`permission` json NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `secret_secret_unique` (`secret`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `secret`
--

LOCK TABLES `secret` WRITE;
/*!40000 ALTER TABLE `secret` DISABLE KEYS */;
INSERT INTO `secret` VALUES (1,'7f46165474d11ee5836777d85df2cdab','Mobile','{}','2018-12-25 10:10:10','2018-12-25 10:10:10'),(2,'3d4fe7a00bc6fb52a91685d038733d6f','Web','{}','2018-12-25 10:10:10','2018-12-25 10:10:10');
/*!40000 ALTER TABLE `secret` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `todo`
--

DROP TABLE IF EXISTS `todo`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `todo` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `todo_user_id_foreign` (`user_id`),
CONSTRAINT `todo_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `todo`
--

LOCK TABLES `todo` WRITE;
/*!40000 ALTER TABLE `todo` DISABLE KEYS */;
/*!40000 ALTER TABLE `todo` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `users`
--

DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `users`
--

LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Dumping events for database 'fastify'
--

--
-- Dumping routines for database 'fastify'
--
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2018-12-25 10:44:24

Apa guna table secret? Gunanya adalah untuk menyimpan secret key yang akan diakses oleh application kita nantinya, sehingga kita bisa lihat user A menggunakan Secret Key yang mana. Apakah Development Key? Terus permission apa saja yang bisa dikasih? untuk kali ini saya gaakan pake fungsi permission dulu. Sedangkan kolom permission ini diibaratkan seperti kolom GRANT pada Laravel Passport bagi yang pernah menggunakan.

Oke kalo udah di import file SQL nya kita bisa mulai ngoding.

Pertama, buat dulu folder dan kasih nama latihan-fastify.

Kedua, lakukan init dan isi data seperti biasa. Enter-enter aja kalo mau cepet karena ini cuma latihan.

npm init

Ketiga, install dulu fastify dan library yang kita butuhkan dengan cara

npm install --save fastify fastify-formbody mysql moment dotenv sha1

Sebelum mulai buat dulu file dengan tipe .env, gunanya untuk nyimpen key Environment aplikasi kalian. Yang pernah pake Laravel pasti paham deh

APP_PORT=3000DB_HOST=192.168.10.10
DB_DATABASE=fastify
DB_NAME=homestead
DB_PASSWORD=secret

Okeh kalo udah keinstall dan udah buat file .env saatnya buat file root yang akan jadi file untuk dijalankan terus, kasih nama fastify.js dan masukkin code dibawah

require('dotenv').config();

// Inisialisasi awal fastify.
const fastify = require('fastify')({
logger: true //aktifkan ini untuk menerima log setiap request dari fastify.
});

//Fungsi ini untuk membuat kita bisa melakuakn post melalui www-url-encoded.
fastify.register(require('fastify-formbody'));

//Route yang dipisah dari root file.
fastify.register(require('./routes'));

//Fungsi file root secara async.
const start = async () => {
try {
//Gunakan Port dari ENV APP_PORT, kalo ngga ada variable tersebut maka akan menggunakan port 3000
await fastify.listen(process.env.APP_PORT || 3000);

fastify.log.info(`server listening on ${fastify.server.address().port}`)
} catch (err) {
fastify.log.error(err);
process.exit(1)
}
};

//Jalankan server!
start();

Jangan langsung dicoba, tunggu dulu, kita akan buat routes buat ujicoba.

Buat file routes.js dan masukkin fungsi dibawah ini

async function routes (fastify, options) {

//Route Ujicoba
fastify.get('/', function (request, reply) {
reply.send({message: 'Hello World', code: 200})
})

}

module.exports = routes;

Nah sekarang coba jalanin dulu filenya dengan cara

node fastify.js

Cus udah jalan gan, test deh ke localhost:3000

Mantap betul kita dah berhasil buat ujicoba fastify nih. Lanjottt.

As usually kita akan pake library mysql. Silahkan buat file connection.js dan copas code dibawah.

var mysql = require('mysql');

var con = mysql.createPool({
connectionLimit : 5,
host: process.env.DB_HOST,
user: process.env.DB_NAMET,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE
});

module.exports = con;

Disini kita akan pake fungsi mysql connection pool, supaya koneksi kalian bisa terus hidup kalo lagi posisi iddle (ngga kepake). Kalo kita pake createConnection akan menyebabkan koneksinya jadi iddle ketika lagi ga kepake dan aplikasi kalian error pas lagi diakses lagi.

Sekarang buat dulu file response.js, kebiasaan dari saya adalah saya selalu membuat inisialisai response body API yang akan digunakan terus-menerus dan hanya perlu diisi isinya saja.

async function ok (values, message, reply) {
return reply
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
.send({
code : 200,
values : values,
message : message,
});
}

async function badRequest (values, message, reply) {
return reply
.code(400)
.header('Content-Type', 'application/json; charset=utf-8')
.send({
code : 400,
values : values,
message : message,
});
}

module.exports = {
ok, badRequest
};

Oke sekarang buat folder controller, didalam folder controller buat file users.js, kita akan membuat registrasi akun terlebih dahulu.

let response = require('../response');
let connection = require('../connection');
let sha1 = require('sha1');
let moment = require('moment');
let crypto = require('crypto');

async function register (request, reply) {

let now = moment().format('YYYY-MM-DD HH:mm:ss').toString();
let name = request.body.name;
let email = request.body.email;
let password = sha1(request.body.password);
let token = crypto.randomBytes(32).toString('hex');
let created_at = now;
let updated_at = now;

let sql = `INSERT INTO users (name, email, password, remember_token, created_at, updated_at)
values(?, ?, ?, ?, ?, ?)`;

//Menggunakan promise apabila membutuhkan data yang akan dikembalikan setelah callback
let data = await new Promise((resolve) =>
connection.query(sql,
[name, email, password, token, created_at, updated_at], function (error, rows) {
if(error){
//Check terlebih dahulu untuk data yang sudah ada.
if(error.code === 'ER_DUP_ENTRY'){
return response.badRequest('', `E-mail ${email} telah digunakan!`, reply)
}

//Jika bukan duplicate entry maka cetak error yang terjadi.
console.log(error);
return response.badRequest('', `${error}`, reply)
}

return resolve({ name: name, email: email, token : token});
})
);

return response.ok(data, `Berhasil registrasi pengguna baru - ${email}`, reply);
}
module.exports = {
register
};

Jika sudah dibuat maka tambahkan di routes menjadi seperti ini

let users = require('./controller/users');

async function routes (fastify, options) {

//Route Ujicoba
fastify.get('/', function (request, reply) {
reply.send({message: 'Hello World', code: 200});
});

fastify.post('/api/users/register', users.register);

}

module.exports = routes;

Jalankan dengan cara node fastify.js dan akses dengan Postman atau Insomnia atau tools API Test kalian.

Contoh return response pada pengguna yang berhasil registrasi.
Contoh request gagal registrasi karena data tersebut sudah ada.

Sekarang kita akan mencoba membuat fungsi login untuk mendapatkan data pengguna.

Silahkan buat fungsi baru di file users.js

async function login(request, reply) {

let email = request.body.email;
let password = request.body.password;
let sql = `SELECT * FROM users WHERE email = ?`;

let data = await new Promise((resolve) =>
connection.query(sql, [email], function (error, rows) {
if(error){
console.log(error);
return response.badRequest('', `${error}`, reply)
}

if(rows.length > 0){
let verify = sha1(password) === rows[0].password;

let data = {
name: rows[0].name,
email: rows[0].email,
token: rows[0].remember_token
};

return verify ? resolve(data) : resolve(false);
}
else{
return resolve(false);
}
})
);

if(!data){
return response.badRequest('','Email atau password yang anda masukkan salah!', reply)
}

return response.ok(data, `Berhasil login!`, reply);
}

Jangan lupa tambahkan function login pada function yang akan di exports.

module.exports = {
register, login
};

Kalau udah silahkan masukkan ke routes kalian.

fastify.post('/api/users/login', users.login);

Nah jalanin lagi, kita test.

Contoh response berhasil login.
Contoh response apabila email atau password salah.

Oke sekarang silahkan buka DB kamu dan lihat secret Key yang telah saya buat.

Secret key di database yang telah saya buat.

Nah darimana secret key ini dibuat? Mungkin ada yang cukup kritis dan bertanya-tanya dalam pikiran agan-agan sekalian, jawabannya secret key ini dibuat secara random aja, agan boleh nulis secret key agan pake nama agan, atau encode dari md5, bisa juga hasil dari base64. Pokoknya bebas agan ngebuat secret key ini.

Oke sekarang kita lanjut ngebuat method untuk ngebuat token yang akan kita pake nanti also known as authentication.

silahkan buat file auth.js didalam file controller dan copas code dibawah.

let response = require('../response');
let connection = require('../connection');
let moment = require('moment');
let crypto = require('crypto');

async function createToken (request, reply) {

let now = moment().format('YYYY-MM-DD HH:mm:ss').toString();
let secret = request.body.secret;
let token = request.body.token;
let created_at = now;
let updated_at = now;

//Mengambil id pengguna
let user_id = await getUser(token);

//Mengambil id secret key
let secret_id = await getSecret(secret);

//Kedua id harus ada di tiap table, kalau tidak ada salah satu maka harus dilemparkan message error.
if(!secret_id || !user_id){
return response.badRequest('','Token atau Secret key kamu salah!', reply);
}

//Menciptakan random string sebanyak kurang lebih 20 karakter
let id = crypto.randomBytes(25).toString('hex');

//Membuat tanggal hari ini + 30 hari kedepan untuk masa aktif token.
let expires_at = moment().add(30, 'days').format('YYYY-MM-DD HH:mm:ss').toString();

let sql = `INSERT INTO authentication (id, user_id, secret_id, expires_at, created_at, updated_at)
values(?, ?, ?, ?, ?, ?)`;
let data = await new Promise((resolve) =>
connection.query(sql,
[id, user_id, secret_id, expires_at, created_at, updated_at], function (error, rows) {
if(error){
console.log(error);
return response.badRequest('', `${error}`, reply)
}

let array = {
token : id,
expires_at : expires_at
};

return resolve(array);
})
);

return response.ok(data, `Berhasil membuat autentikasi!`, reply);
}

async function getUser (token) {
return new Promise((resolve) =>
connection.query('SELECT id FROM users WHERE remember_token = ?', [token], function (error, rows) {
if(error){
console.log(error);
}

return rows.length > 0 ? resolve(rows[0].id) : resolve(false);
})
);
}

async function getSecret (secret) {
return new Promise((resolve) =>
connection.query('SELECT id FROM secret WHERE secret = ?', [secret], function (error, rows) {
if(error){
console.log(error);
}

return rows.length > 0 ? resolve(rows[0].id) : resolve(false);
})
);
}

module.exports = {
createToken
};

Oke jangan lupa tambahin ke routes.

fastify.post('/api/token', auth.createToken);

Jalanin lagi dan coba deh dengan Secret Key yang saya berikan di table secret ditambah dengan token yang didapat dari pengguna.

Respon apabila autentikasi berhasil dibuat.
Respon apabila salah satu token atau secret key tidak ada di database.

Oke segini dulu ya, nanti di lanjut ke part 2 untuk pengecekan token, CRUD, hingga middleware untuk pengecekan token.

Semoga bermanfaat, happy coding ^^

--

--

Hudya
Hudya

Written by Hudya

Which is more difficult, coding or counting? Not both of them, the difficult one is sharing your knowledge to people without asking the payment.

Responses (3)