Validasi Formulir Smart Node.js

Diterbitkan: 2022-03-11

Salah satu tugas mendasar yang harus dilakukan dalam API adalah validasi data. Dalam artikel ini, saya ingin menunjukkan cara menambahkan validasi antipeluru untuk data Anda dengan cara yang juga mengembalikannya dengan format yang baik.

Melakukan validasi data kustom di Node.js tidaklah mudah dan juga tidak cepat. Ada banyak fungsi yang perlu Anda tulis untuk mencakup semua jenis data. Meskipun saya telah mencoba beberapa pustaka data formulir Node.js—untuk Express dan Koa—mereka tidak pernah memenuhi kebutuhan proyek saya. Ada masalah dengan perluasan perpustakaan dan perpustakaan tidak bekerja dengan struktur data yang kompleks atau validasi asinkron.

Validasi Formulir di Node.js dengan Datalize

Itu sebabnya saya akhirnya memutuskan untuk menulis perpustakaan validasi formulir saya yang kecil tapi kuat yang disebut datalize. Ini dapat diperpanjang, sehingga Anda dapat menggunakannya dalam proyek apa pun dan menyesuaikannya dengan kebutuhan Anda. Ini memvalidasi badan permintaan, kueri, atau params. Ini juga mendukung filter async dan struktur JSON yang kompleks seperti array atau objek bersarang .

Mempersiapkan

Datalize dapat diinstal melalui npm:

 npm install --save datalize

Untuk mengurai isi permintaan, Anda harus menggunakan perpustakaan terpisah. Jika Anda belum menggunakannya, saya sarankan koa-body untuk Koa atau body-parser untuk Express.

Anda dapat menerapkan tutorial ini ke server API HTTP yang sudah Anda siapkan, atau gunakan kode server HTTP Koa sederhana berikut.

 const Koa = require('koa'); const bodyParser = require('koa-body'); const app = new Koa(); const router = new (require('koa-router'))(); // helper for returning errors in routes app.context.error = function(code, obj) { this.status = code; this.body = obj; }; // add koa-body middleware to parse JSON and form-data body app.use(bodyParser({ enableTypes: ['json', 'form'], multipart: true, formidable: { maxFileSize: 32 * 1024 * 1024, } })); // Routes... // connect defined routes as middleware to Koa app.use(router.routes()); // our app will listen on port 3000 app.listen(3000); console.log(' API listening on 3000');

Namun, ini bukan pengaturan produksi (Anda harus menggunakan logging, menegakkan otorisasi, menangani kesalahan, dll.), tetapi beberapa baris kode ini akan berfungsi dengan baik untuk contoh yang akan saya tunjukkan kepada Anda.

Catatan: Semua contoh kode menggunakan Koa, tetapi kode validasi data juga akan berfungsi untuk Express. Pustaka datalize juga memiliki contoh untuk mengimplementasikan validasi formulir Express.

Contoh Validasi Formulir Node.js Dasar

Katakanlah Anda memiliki server web Koa atau Express dan titik akhir di API Anda yang membuat pengguna dengan beberapa bidang dalam database. Beberapa bidang diperlukan, dan beberapa hanya dapat memiliki nilai tertentu atau harus diformat untuk jenis yang benar.

Anda bisa menulis logika sederhana seperti ini:

 /** * @api {post} / Create a user * ... */ router.post('/', (ctx) => { const data = ctx.request.body; const errors = {}; if (!String(data.name).trim()) { errors.name = ['Name is required']; } if (!(/^[\-0-9a-zA-Z\.\+_]+@[\-0-9a-zA-Z\.\+_]+\.[a-zA-Z]{2,}$/).test(String(data.email))) { errors.email = ['Email is not valid.']; } if (Object.keys(errors).length) { return ctx.error(400, {errors}); } const user = await User.create({ name: data.name, email: data.email, }); ctx.body = user.toJSON(); });

Sekarang mari kita tulis ulang kode ini dan validasi permintaan ini menggunakan datalize:

 const datalize = require('datalize'); const field = datalize.field; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email(), ]), (ctx) => { if (!ctx.form.isValid) { return ctx.error(400, {errors: ctx.form.errors}); } const user = await User.create(ctx.form); ctx.body = user.toJSON(); });

Lebih pendek, lebih bersih, sehingga mudah dibaca. Dengan datalize, Anda dapat menentukan daftar bidang dan menghubungkannya dengan banyak aturan (fungsi yang menimbulkan kesalahan jika input tidak valid) atau memfilter (fungsi yang memformat input) sesuai keinginan.

Aturan dan filter dijalankan dalam urutan yang sama seperti yang ditentukan, jadi jika Anda ingin memangkas string untuk spasi terlebih dahulu dan kemudian memeriksa apakah ada nilai, Anda harus mendefinisikan .trim() sebelum .required() .

Datalize kemudian akan membuat objek (tersedia sebagai .form dalam objek konteks yang lebih luas) hanya dengan bidang yang telah Anda tentukan, jadi Anda tidak perlu mencantumkannya lagi. Properti .form.isValid memberi tahu Anda apakah validasi berhasil atau tidak.

Penanganan Kesalahan Otomatis

Jika kita tidak ingin memeriksa apakah formulir itu valid atau tidak untuk setiap permintaan, kita dapat menambahkan middleware global yang membatalkan permintaan jika data tidak lolos validasi.

Untuk melakukan ini, kami hanya menambahkan potongan kode ini ke file bootstrap kami di mana kami membuat instance aplikasi Koa/Express kami.

 const datalize = require('datalize'); // set datalize to throw an error if validation fails datalize.set('autoValidate', true); // only Koa // add to very beginning of Koa middleware chain app.use(async (ctx, next) => { try { await next(); } catch (err) { if (err instanceof datalize.Error) { ctx.status = 400; ctx.body = err.toJSON(); } else { ctx.status = 500; ctx.body = 'Internal server error'; } } }); // only Express // add to very end of Express middleware chain app.use(function(err, req, res, next) { if (err instanceof datalize.Error) { res.status(400).send(err.toJSON()); } else { res.send(500).send('Internal server error'); } });

Dan kita tidak perlu memeriksa apakah data valid lagi, karena datalize akan melakukannya untuk kita. Jika data tidak valid, itu akan mengembalikan pesan kesalahan yang diformat dengan daftar bidang yang tidak valid.

Validasi Kueri

Ya, Anda bahkan dapat memvalidasi parameter kueri Anda dengan sangat mudah—tidak harus digunakan hanya dengan permintaan POST . Kami hanya menggunakan metode pembantu .query() , dan satu-satunya perbedaan adalah bahwa data disimpan dalam objek .data bukan .form .

 const datalize = require('datalize'); const field = datalize.field; /** * @api {get} / List users * ... */ router.post('/', datalize.query([ field('keywords').trim(), field('page').default(1).number(), field('perPage').required().select([10, 30, 50]), ]), (ctx) => { const limit = ctx.data.perPage; const where = { }; if (ctx.data.keywords) { where.name = {[Op.like]: ctx.data.keywords + '%'}; } const users = await User.findAll({ where, limit, offset: (ctx.data.page - 1) * limit, }); ctx.body = users; });

Ada juga metode pembantu untuk validasi parameter, .params() . Data kueri dan formulir dapat divalidasi bersama dengan melewatkan dua middlewares data dalam metode .post() router.

Lebih Banyak Filter, Array, dan Objek Bersarang

Sejauh ini kami telah menggunakan data yang sangat sederhana dalam validasi formulir Node.js kami. Sekarang mari kita coba beberapa bidang yang lebih kompleks seperti array, objek bersarang, dll.:

 const datalize = require('datalize'); const field = datalize.field; const DOMAIN_ERROR = "Email's domain does not have a valid MX (mail) entry in its DNS record"; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email().custom((value) => { return new Promise((resolve, reject) => { dns.resolve(value.split('@')[1], 'MX', function(err, addresses) { if (err || !addresses || !addresses.length) { return reject(new Error(DOMAIN_ERROR)); } resolve(); }); }); }), field('type').required().select(['admin', 'user']), field('languages').array().container([ field('id').required().id(), field('level').required().select(['beginner', 'intermediate', 'advanced']) ]), field('groups').array().id(), ]), async (ctx) => { const {languages, groups} = ctx.form; delete ctx.form.languages; delete ctx.form.groups; const user = await User.create(ctx.form); await UserGroup.bulkCreate(groups.map(groupId => ({ groupId, userId: user.id, }))); await UserLanguage.bulkCreate(languages.map(item => ({ languageId: item.id, userId: user.id, level: item.level, )); });

Jika tidak ada aturan bawaan untuk data yang perlu kita validasi, kita bisa membuat aturan validasi data kustom dengan metode .custom() (nama bagus, kan?) dan menulis logika yang diperlukan di sana. Untuk objek bersarang, ada metode .container() di mana Anda bisa menentukan daftar bidang dengan cara yang sama seperti pada fungsi datalize() . Anda dapat menyarangkan container di dalam container atau melengkapinya dengan filter .array() , yang mengonversi nilai menjadi array. Saat filter .array() digunakan tanpa wadah, aturan atau filter yang ditentukan akan diterapkan ke setiap nilai dalam array.

Jadi .array().select(['read', 'write']) akan memeriksa apakah setiap nilai dalam array adalah 'read' atau 'write' dan jika ada yang tidak, itu akan mengembalikan daftar semua indeks dengan kesalahan. Cukup keren, ya?

PUT / PATCH

Saat memperbarui data Anda dengan PUT / PATCH (atau POST ), Anda tidak perlu menulis ulang semua logika, aturan, dan filter Anda. Anda cukup menambahkan filter tambahan seperti .optional() atau .patch() , yang akan menghapus bidang apa pun dari objek konteks jika tidak ditentukan dalam permintaan. ( .optional() akan membuatnya selalu opsional, sedangkan .patch() akan menjadikannya opsional hanya jika metode permintaan HTTP adalah PATCH .) Anda dapat menambahkan filter ekstra ini sehingga berfungsi untuk membuat dan memperbarui data di database Anda.

 const datalize = require('datalize'); const field = datalize.field; const userValidator = datalize([ field('name').patch().trim().required(), field('email').patch().required().email(), field('type').patch().required().select(['admin', 'user']), ]); const userEditMiddleware = async (ctx, next) => { const user = await User.findByPk(ctx.params.id); // cancel request here if user was not found if (!user) { throw new Error('User was not found.'); } // store user instance in the request so we can use it later ctx.user = user; return next(); }; /** * @api {post} / Create a user * ... */ router.post('/', userValidator, async (ctx) => { const user = await User.create(ctx.form); ctx.body = user.toJSON(); }); /** * @api {put} / Update a user * ... */ router.put('/:id', userEditMiddleware, userValidator, async (ctx) => { await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); }); /** * @api {patch} / Patch a user * ... */ router.patch('/:id', userEditMiddleware, userValidator, async (ctx) => { if (!Object.keys(ctx.form).length) { return ctx.error(400, {message: 'Nothing to update.'}); } await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); });

Dengan dua middlewares sederhana, kita dapat menulis sebagian besar logika untuk semua metode POST / PUT / PATCH . Fungsi userEditMiddleware() memverifikasi apakah catatan yang ingin kita edit ada dan menimbulkan kesalahan sebaliknya. Kemudian userValidator() melakukan validasi untuk semua titik akhir. Terakhir, filter .patch() akan menghapus bidang apa pun dari objek .form jika tidak ditentukan dan jika metode permintaannya adalah PATCH .

Ekstra Validasi Formulir Node.js

Di filter khusus, Anda bisa mendapatkan nilai bidang lain dan melakukan validasi berdasarkan itu. Anda juga bisa mendapatkan data apa pun dari objek konteks, seperti permintaan atau informasi pengguna, karena semuanya disediakan dalam parameter panggilan balik fungsi kustom.

Pustaka mencakup seperangkat aturan dan filter dasar, tetapi Anda dapat mendaftarkan filter global kustom yang dapat Anda gunakan dengan bidang apa pun, sehingga Anda tidak perlu menulis kode yang sama berulang kali:

 const datalize = require('datalize'); const Field = datalize.Field; Field.prototype.date = function(format = 'YYYY-MM-DD') { return this.add(function(value) { const date = value ? moment(value, format) : null; if (!date || !date.isValid()) { throw new Error('%s is not a valid date.'); } return date.format(format); }); }; Field.prototype.dateTime = function(format = 'YYYY-MM-DD HH:mm') { return this.date(format); };

Dengan dua filter khusus ini, Anda dapat menyambungkan bidang dengan .date() atau .dateTime() untuk memvalidasi input tanggal.

File juga dapat divalidasi menggunakan datalize: Ada filter khusus hanya untuk file seperti .file() , .mime() , dan .size() sehingga Anda tidak perlu menangani file secara terpisah.

Mulai Menulis API yang Lebih Baik Sekarang

Saya telah menggunakan datalize untuk validasi formulir Node.js di beberapa proyek produksi, baik untuk API kecil maupun besar. Ini membantu saya untuk memberikan proyek-proyek hebat tepat waktu dan dengan lebih sedikit stres sambil membuatnya lebih mudah dibaca dan dipelihara. Pada satu proyek saya bahkan menggunakannya untuk memvalidasi data untuk pesan WebSocket dengan menulis pembungkus sederhana di sekitar Socket.IO dan penggunaannya hampir sama dengan mendefinisikan rute di Koa, jadi itu bagus. Jika ada cukup minat, saya mungkin menulis tutorial untuk itu juga.

Saya harap tutorial ini akan membantu Anda dan saya membangun API yang lebih baik di Node.js, dengan data yang divalidasi dengan sempurna tanpa masalah keamanan atau kesalahan server internal. Dan yang paling penting, saya harap ini akan menghemat banyak waktu yang seharusnya Anda investasikan untuk menulis fungsi tambahan untuk validasi formulir menggunakan JavaScript.