Membuat API GraphQL Pertama Anda
Diterbitkan: 2022-03-11Kata pengantar
Beberapa tahun yang lalu, Facebook memperkenalkan cara baru untuk membangun API back-end yang disebut GraphQL, yang pada dasarnya adalah bahasa khusus domain untuk kueri dan manipulasi data. Pada awalnya, saya tidak terlalu memperhatikannya, tetapi akhirnya, saya menemukan diri saya terlibat dengan sebuah proyek di Toptal, di mana saya harus mengimplementasikan API back-end berdasarkan GraphQL. Saat itulah saya melanjutkan dan belajar bagaimana menerapkan pengetahuan yang saya pelajari untuk REST ke GraphQL.
Itu adalah pengalaman yang sangat menarik, dan selama periode implementasi, saya harus memikirkan kembali pendekatan dan metodologi standar yang digunakan dalam REST API dengan cara yang lebih ramah GraphQL. Dalam artikel ini, saya mencoba merangkum masalah umum yang perlu dipertimbangkan saat mengimplementasikan GraphQL API untuk pertama kalinya.
Perpustakaan yang Diperlukan
GraphQL dikembangkan secara internal oleh Facebook dan dirilis secara publik pada tahun 2015. Kemudian pada tahun 2018, proyek GraphQL dipindahkan dari Facebook ke GraphQL Foundation yang baru didirikan, diselenggarakan oleh Linux Foundation nirlaba, yang memelihara dan mengembangkan spesifikasi bahasa kueri GraphQL dan referensi implementasi untuk JavaScript.
Karena GraphQL masih merupakan teknologi muda dan implementasi referensi awal tersedia untuk JavaScript, sebagian besar pustaka yang matang untuk itu ada di ekosistem Node.js. Ada juga dua perusahaan lain, Apollo dan Prisma, yang menyediakan alat dan pustaka sumber terbuka untuk GraphQL. Contoh proyek dalam artikel ini akan didasarkan pada implementasi referensi GraphQL untuk JavaScript dan perpustakaan yang disediakan oleh dua perusahaan ini:
- Graphql-js – Implementasi referensi GraphQL untuk JavaScript
- Apollo-server – Server GraphQL untuk Express, Connect, Hapi, Koa, dan banyak lagi
- Apollo-graphql-tools – Membangun, meniru, dan menjahit skema GraphQL menggunakan SDL
- Prisma-graphql-middleware – Pisahkan resolver GraphQL Anda dalam fungsi middleware
Di dunia GraphQL, Anda mendeskripsikan API Anda menggunakan skema GraphQL, dan untuk ini, spesifikasi mendefinisikan bahasanya sendiri yang disebut The GraphQL Schema Definition Language (SDL). SDL sangat sederhana dan intuitif untuk digunakan sekaligus menjadi sangat kuat dan ekspresif.
Ada dua cara untuk membuat skema GraphQL: pendekatan kode-pertama dan pendekatan skema-pertama.
- Dalam pendekatan kode-pertama, Anda menggambarkan skema GraphQL Anda sebagai objek JavaScript berdasarkan pustaka graphql-js, dan SDL dibuat secara otomatis dari kode sumber.
- Dalam pendekatan skema-pertama, Anda menjelaskan skema GraphQL Anda di SDL dan menghubungkan logika bisnis Anda menggunakan perpustakaan alat graphql Apollo.
Secara pribadi, saya lebih suka pendekatan skema-pertama dan akan menggunakannya untuk proyek sampel dalam artikel ini. Kami akan menerapkan contoh toko buku klasik dan membuat back end yang akan menyediakan CRUD API untuk membuat penulis dan buku ditambah API untuk manajemen dan otentikasi pengguna.
Membuat Server GraphQL Dasar
Untuk menjalankan server GraphQL dasar, kita harus membuat proyek baru, menginisialisasinya dengan npm, dan mengkonfigurasi Babel. Untuk mengkonfigurasi Babel, pertama-tama instal pustaka yang diperlukan dengan perintah berikut:
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node
Setelah menginstal Babel, buat file dengan nama .babelrc
di direktori root proyek kami dan salin konfigurasi berikut di sana:
{ "presets": [ [ "@babel/env", { "targets": { "node": "current" } } ] ] }
Edit juga file package.json
dan tambahkan perintah berikut ke bagian scripts
:
{ ... "scripts": { "serve": "babel-node index.js" }, ... }
Setelah kami mengonfigurasi Babel, instal pustaka GraphQL yang diperlukan dengan perintah berikut:
npm install --save express apollo-server-express graphql graphql-tools graphql-tag
Setelah menginstal pustaka yang diperlukan, untuk menjalankan server GraphQL dengan pengaturan minimal, salin cuplikan kode ini di file index.js
kami:
import gql from 'graphql-tag'; import express from 'express'; import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'; const port = process.env.PORT || 8080; // Define APIs using GraphQL SDL const typeDefs = gql` type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } }; // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolvers maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });
Setelah ini, kita dapat menjalankan server kita menggunakan perintah npm run serve
, dan jika kita menavigasi di browser web ke URL http://localhost:8080/graphql
, shell visual interaktif GraphQL, yang disebut Playground, akan terbuka, di mana kita bisa jalankan kueri dan mutasi GraphQL dan lihat data hasilnya.
Di dunia GraphQL, fungsi API dibagi menjadi tiga set, yang disebut kueri, mutasi, dan langganan:
- Query digunakan oleh klien untuk meminta data yang dibutuhkan dari server.
- Mutasi digunakan oleh klien untuk membuat/memperbarui/menghapus data di server.
- Langganan digunakan oleh klien untuk membuat dan memelihara koneksi real-time ke server. Hal ini memungkinkan klien untuk mendapatkan peristiwa dari server dan bertindak sesuai.
Dalam artikel kami, kami hanya akan membahas pertanyaan dan mutasi. Langganan adalah topik yang sangat besar—mereka layak mendapatkan artikel mereka sendiri dan tidak diperlukan dalam setiap implementasi API.
Tipe Data Skalar Tingkat Lanjut
Segera setelah bermain dengan GraphQL, Anda akan menemukan bahwa SDL hanya menyediakan tipe data primitif, dan tipe data skalar lanjutan seperti Tanggal, Waktu, dan DateTime, yang merupakan bagian penting dari setiap API, tidak ada. Untungnya, kami memiliki perpustakaan yang membantu kami memecahkan masalah ini, dan itu disebut graphql-iso-date. Setelah menginstalnya, kita perlu mendefinisikan tipe data skalar lanjutan baru dalam skema kita dan menghubungkannya ke implementasi yang disediakan oleh perpustakaan:
import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date'; // Define APIs using GraphQL SDL const typeDefs = gql` scalar Date scalar Time scalar DateTime type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Date: GraphQLDate, Time: GraphQLTime, DateTime: GraphQLDateTime, Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } };
Seiring dengan tanggal dan waktu, ada juga implementasi tipe data skalar menarik lainnya, yang dapat berguna bagi Anda tergantung pada kasus penggunaan Anda. Misalnya, salah satunya adalah graphql-type-json, yang memberi kita kemampuan untuk menggunakan pengetikan dinamis dalam skema GraphQL kita dan meneruskan atau mengembalikan objek JSON yang tidak diketik menggunakan API kita. Ada juga library graphql-scalar, yang memberi kita kemampuan untuk mendefinisikan skalar GraphQL kustom dengan sanitasi/validasi/transformasi lanjutan.
Jika diperlukan, Anda juga dapat menentukan tipe data skalar kustom dan menggunakannya dalam skema Anda, seperti yang ditunjukkan di atas. Ini tidak sulit, tetapi pembahasannya berada di luar cakupan artikel ini—jika tertarik, Anda dapat menemukan informasi lebih lanjut dalam dokumentasi Apollo.
Skema Pemisahan
Setelah menambahkan lebih banyak fungsionalitas ke skema Anda, skema itu akan mulai berkembang dan kami akan memahami bahwa tidak mungkin menyimpan seluruh rangkaian definisi dalam satu file, dan kami perlu membaginya menjadi potongan-potongan kecil untuk mengatur kode dan membuatnya lebih skalabel untuk ukuran yang lebih besar. Untungnya fungsi pembuat skema makeExecutableSchema
, yang disediakan oleh Apollo, juga menerima definisi skema dan peta resolver dalam bentuk array. Ini memberi kita kemampuan untuk membagi skema dan peta resolver kita menjadi bagian-bagian yang lebih kecil. Inilah tepatnya yang telah saya lakukan dalam proyek sampel saya; Saya telah membagi API menjadi beberapa bagian berikut:
-
auth.api.graphql
– API untuk otentikasi dan pendaftaran pengguna -
author.api.graphql
– CRUD API untuk entri penulis -
book.api.graphql
– CRUD API untuk entri buku -
root.api.graphql
– Akar skema dan definisi umum (seperti tipe skalar lanjutan) -
user.api.graphql
– CRUD API untuk manajemen pengguna
Selama skema pemisahan, ada satu hal yang harus kita pertimbangkan. Salah satu bagian harus menjadi skema root dan yang lain harus memperluas skema root. Ini terdengar rumit, tetapi pada kenyataannya itu cukup sederhana. Dalam skema root, kueri dan mutasi didefinisikan seperti ini:
type Query { ... } type Mutation { ... }
Dan di yang lain, mereka didefinisikan seperti ini:
extend type Query { ... } extend type Mutation { ... }
Dan itu saja.
Otentikasi dan Otorisasi
Di sebagian besar implementasi API, ada persyaratan untuk membatasi akses global dan menyediakan semacam kebijakan akses berbasis aturan. Untuk ini, kami harus memperkenalkan dalam kode kami: Otentikasi —untuk mengonfirmasi identitas pengguna—dan Otorisasi , untuk menegakkan kebijakan akses berbasis aturan.
Di dunia GraphQL, seperti dunia REST, umumnya untuk otentikasi kami menggunakan JSON Web Token. Untuk memvalidasi token JWT yang diteruskan, kita perlu mencegat semua permintaan yang masuk dan memeriksa header otorisasi pada mereka. Untuk ini, selama pembuatan server Apollo, kita dapat mendaftarkan fungsi sebagai kait konteks, yang akan dipanggil dengan permintaan saat ini yang membuat konteks yang dibagikan di semua resolver. Ini dapat dilakukan seperti ini:
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, context: ({ req, res }) => { const context = {}; // Verify jwt token const parts = req.headers.authorization ? req.headers.authorization.split(' ') : ['']; const token = parts.length === 2 && parts[0].toLowerCase() === 'bearer' ? parts[1] : undefined; context.authUser = token ? verify(token) : undefined; return context; } }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });
Di sini, jika pengguna akan memberikan token JWT yang benar, kami memverifikasinya dan menyimpan objek pengguna dalam konteks, yang akan dapat diakses oleh semua resolver selama eksekusi permintaan.
Kami memverifikasi identitas pengguna, tetapi API kami masih dapat diakses secara global dan tidak ada yang mencegah pengguna kami untuk memanggilnya tanpa otorisasi. Salah satu cara untuk mencegahnya adalah dengan memeriksa objek pengguna dalam konteks secara langsung di setiap resolver, tetapi ini adalah pendekatan yang sangat rawan kesalahan karena kita harus menulis banyak kode boilerplate dan kita bisa lupa menambahkan tanda centang saat menambahkan resolver baru . Jika kita melihat kerangka REST API, umumnya masalah seperti itu diselesaikan dengan menggunakan pencegat permintaan HTTP, tetapi dalam kasus GraphQL, itu tidak masuk akal karena satu permintaan HTTP dapat berisi beberapa kueri GraphQL, dan jika kita masih menambahkan itu, kami hanya mendapatkan akses ke representasi string mentah dari kueri dan harus menguraikannya secara manual, yang jelas bukan pendekatan yang baik. Konsep ini tidak diterjemahkan dengan baik dari REST ke GraphQL.
Jadi kita perlu semacam cara untuk mencegat kueri GraphQL, dan cara ini disebut prisma-graphql-middleware. Pustaka ini memungkinkan kita menjalankan kode arbitrer sebelum atau setelah resolver dipanggil. Ini meningkatkan struktur kode kami dengan mengaktifkan penggunaan kembali kode dan pemisahan yang jelas dari masalah.
Komunitas GraphQL telah membuat sekumpulan middleware mengagumkan berdasarkan perpustakaan middleware Prisma, yang memecahkan beberapa kasus penggunaan tertentu, dan untuk otorisasi pengguna, terdapat perpustakaan bernama graphql-shield, yang membantu kami membuat lapisan izin untuk API kami.
Setelah menginstal graphql-shield, kami dapat memperkenalkan lapisan izin untuk API kami seperti ini:
import { allow } from 'graphql-shield'; const isAuthorized = rule()( (obj, args, { authUser }, info) => authUser && true ); export const permissions = { Query: { '*': isAuthorized, sayHello: allow }, Mutation: { '*': isAuthorized, sayHello: allow } }
Dan kita dapat menerapkan lapisan ini sebagai middleware untuk skema kita seperti ini:
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })
Di sini saat membuat objek perisai, kami menyetel allowExternalErrors
ke true, karena secara default, perilaku perisai adalah menangkap dan menangani kesalahan yang terjadi di dalam resolver, dan ini bukan perilaku yang dapat diterima untuk aplikasi sampel saya.

Dalam contoh di atas, kami hanya membatasi akses ke API kami untuk pengguna yang diautentikasi, tetapi perisai sangat fleksibel dan, dengan menggunakannya, kami dapat menerapkan skema otorisasi yang sangat kaya untuk pengguna kami. Misalnya, dalam contoh aplikasi kami, kami memiliki dua peran: USER
dan USER_MANAGER
, dan hanya pengguna dengan peran USER_MANAGER
dapat memanggil fungsionalitas administrasi pengguna. Ini diimplementasikan seperti ini:
export const isUserManager = rule()( (obj, args, { authUser }, info) => authUser && authUser.role === 'USER_MANAGER' ); export const permissions = { Query: { userById: isUserManager, users: isUserManager }, Mutation: { editUser: isUserManager, deleteUser: isUserManager } }
Satu hal lagi yang ingin saya sebutkan adalah bagaimana mengatur fungsi middleware dalam proyek kita. Seperti definisi skema dan peta resolver, lebih baik untuk membaginya per skema dan menyimpannya dalam file terpisah, tetapi tidak seperti server Apollo, yang menerima larik definisi skema dan peta resolver dan menggabungkannya untuk kami, perpustakaan middleware Prisma tidak melakukan ini dan hanya menerima satu objek peta middleware, jadi jika kita membaginya, kita harus menjahitnya kembali secara manual. Untuk melihat solusi saya untuk masalah ini, silakan lihat kelas ApiExplorer
di proyek sampel.
Validasi
GraphQL SDL menyediakan fungsionalitas yang sangat terbatas untuk memvalidasi input pengguna; kami hanya dapat menentukan bidang mana yang diperlukan dan mana yang opsional. Setiap persyaratan validasi lebih lanjut, kita harus menerapkan secara manual. Kami dapat menerapkan aturan validasi secara langsung di fungsi resolver, tetapi fungsi ini sebenarnya tidak termasuk di sini, dan ini adalah kasus penggunaan hebat lainnya untuk middlewares GraphQL pengguna. Sebagai contoh, mari kita gunakan data input permintaan pendaftaran pengguna, di mana kita harus memvalidasi apakah nama pengguna adalah alamat email yang benar, jika input kata sandi cocok, dan kata sandinya cukup kuat. Ini dapat diimplementasikan seperti ini:
import { UserInputError } from 'apollo-server-express'; import passwordValidator from 'password-validator'; import { isEmail } from 'validator'; const passwordSchema = new passwordValidator() .is().min(8) .is().max(20) .has().letters() .has().digits() .has().symbols() .has().not().spaces(); export const validators = { Mutation: { signup: (resolve, parent, args, context) => { const { email, password, rePassword } = args.signupReq; if (!isEmail(email)) { throw new UserInputError('Invalid Email address!'); } if (password !== rePassword) { throw new UserInputError('Passwords don\'t match!'); } if (!passwordSchema.validate(password)) { throw new UserInputError('Password is not strong enough!'); } return resolve(parent, args, context); } } }
Dan kita dapat menerapkan lapisan validator sebagai middleware ke skema kita, bersama dengan lapisan izin seperti ini:
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, validators, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app })
N + 1 Pertanyaan
Masalah lain yang perlu dipertimbangkan, yang terjadi dengan GraphQL API dan sering diabaikan, adalah kueri N+1. Masalah ini terjadi ketika kita memiliki hubungan satu-ke-banyak antara tipe yang didefinisikan dalam skema kita. Untuk mendemonstrasikannya, misalnya, mari gunakan API buku dari proyek sampel kami:
extend type Query { books: [Book!]! ... } extend type Mutation { ... } type Book { id: ID! creator: User! createdAt: DateTime! updatedAt: DateTime! authors: [Author!]! title: String! about: String language: String genre: String isbn13: String isbn10: String publisher: String publishDate: Date hardcover: Int } type User { id: ID! createdAt: DateTime! updatedAt: DateTime! fullName: String! email: String! }
Di sini, kita melihat jenis User
memiliki hubungan satu-ke-banyak dengan jenis Book
, dan hubungan ini direpresentasikan sebagai bidang pembuat di Book
. Peta resolver untuk skema ini didefinisikan seperti ini:
export const resolvers = { Query: { books: (obj, args, context, info) => { return bookService.findAll(); }, ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { return userService.findById(creatorId); }, ... } }
Jika kita menjalankan kueri buku menggunakan API ini, dan melihat log pernyataan SQL, kita akan melihat sesuatu seperti ini:
select `books`.* from `books` select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? ...
Mudah ditebak—selama eksekusi, resolver pertama kali dipanggil untuk kueri buku, yang mengembalikan daftar buku dan kemudian setiap objek buku disebut penyelesai bidang pembuat, dan perilaku ini menyebabkan kueri basis data N + 1. Jika kita tidak ingin meledakkan database kita, perilaku seperti itu tidak terlalu bagus.
Untuk mengatasi masalah kueri N + 1, pengembang Facebook membuat solusi yang sangat menarik yang disebut DataLoader, yang dijelaskan di halaman README-nya seperti ini:
“DataLoader adalah utilitas umum untuk digunakan sebagai bagian dari lapisan pengambilan data aplikasi Anda untuk menyediakan API yang disederhanakan dan konsisten melalui berbagai sumber data jarak jauh seperti database atau layanan web melalui batching dan caching”
Tidaklah mudah untuk memahami cara kerja DataLoader, jadi pertama-tama mari kita lihat contoh yang memecahkan masalah yang ditunjukkan di atas dan kemudian menjelaskan logika di baliknya.
Dalam proyek sampel kami, DataLoader didefinisikan seperti ini untuk bidang pembuat:
export class UserDataLoader extends DataLoader { constructor() { const batchLoader = userIds => { return userService .findByIds(userIds) .then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) ); }; super(batchLoader); } static getInstance(context) { if (!context.userDataLoader) { context.userDataLoader = new UserDataLoader(); } return context.userDataLoader; } }
Setelah kami mendefinisikan UserDataLoader, kami dapat mengubah penyelesai bidang pembuat seperti ini:
export const resolvers = { Query: { ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { const userDataLoader = UserDataLoader.getInstance(context); return userDataLoader.load(creatorId); }, ... } }
Setelah perubahan yang diterapkan, jika kita menjalankan kueri buku lagi dan melihat log pernyataan SQL, kita akan melihat sesuatu seperti ini:
select `books`.* from `books` select `users`.* from `users` where `id` in (?)
Di sini, kita dapat melihat bahwa kueri basis data N + 1 dikurangi menjadi dua kueri, di mana yang pertama memilih daftar buku dan yang kedua memilih daftar pengguna yang disajikan sebagai pembuat dalam daftar buku. Sekarang mari kita jelaskan bagaimana DataLoader mencapai hasil ini.
Fitur utama DataLoader adalah batching. Selama fase eksekusi tunggal, DataLoader akan mengumpulkan semua id yang berbeda dari semua panggilan fungsi beban individu dan kemudian memanggil fungsi batch dengan semua id yang diminta. Satu hal penting untuk diingat adalah bahwa instance DataLoaders tidak dapat digunakan kembali, setelah fungsi batch dipanggil, nilai yang dikembalikan akan di-cache dalam instance selamanya. Karena perilaku ini, kita harus membuat instance baru DataLoader per setiap fase eksekusi. Untuk mencapai ini, kami telah membuat fungsi getInstance
statis, yang memeriksa apakah instance DataLoader disajikan dalam objek konteks dan, jika tidak ditemukan, membuatnya. Ingat bahwa objek konteks baru dibuat untuk setiap fase eksekusi dan dibagikan ke semua resolver.
Fungsi pemuatan batch dari DataLoader menerima larik ID yang diminta berbeda dan mengembalikan janji yang diselesaikan ke larik objek yang sesuai. Saat menulis fungsi pemuatan batch, kita harus mengingat dua hal penting:
- Larik hasil harus sama panjangnya dengan larik ID yang diminta. Misalnya, jika kami meminta ID
[1, 2, 3]
, larik hasil yang dikembalikan harus berisi tepat tiga objek:[{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
- Setiap indeks dalam larik hasil harus sesuai dengan indeks yang sama dalam larik ID yang diminta. Misalnya, jika larik ID yang diminta memiliki urutan sebagai berikut:
[3, 1, 2]
, maka larik hasil yang dikembalikan harus berisi objek dengan urutan yang sama persis:[{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]
Dalam contoh kami, kami memastikan bahwa urutan hasil cocok dengan urutan ID yang diminta dengan kode berikut:
then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) )
Keamanan
Dan last but not least, saya ingin menyebutkan keamanan. Dengan GraphQL, kita dapat membuat API yang sangat fleksibel dan memberikan kemampuan yang kaya kepada pengguna tentang cara mengkueri data. Ini memberikan cukup banyak kekuatan ke sisi klien dari aplikasi dan, seperti yang dikatakan Paman Ben, "Dengan kekuatan besar, datang tanggung jawab besar." Tanpa keamanan yang tepat, pengguna jahat dapat mengirimkan kueri yang mahal dan menyebabkan serangan DoS (Denial of Service) di server kami.
Hal pertama yang dapat kita lakukan untuk melindungi API kita adalah dengan menonaktifkan introspeksi skema GraphQL. Secara default, server GraphQL API memperlihatkan kemampuan untuk mengintrospeksi seluruh skemanya, yang umumnya digunakan oleh cangkang visual interaktif seperti GraphiQL dan Apollo Playground, tetapi juga bisa sangat berguna bagi pengguna jahat untuk membuat kueri kompleks berdasarkan API kami . Kita dapat menonaktifkan ini dengan menyetel parameter introspection
ke false saat membuat Server Apollo:
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })
Hal berikutnya yang dapat kita lakukan untuk melindungi API kita adalah dengan membatasi kedalaman kueri. Ini sangat penting jika kita memiliki hubungan siklik antara tipe data kita. Misalnya, dalam sampel kami, Author
jenis proyek memiliki buku bidang, dan jenis Book
memiliki penulis bidang. Ini jelas merupakan hubungan siklik, dan tidak ada yang mencegah pengguna jahat menulis kueri seperti ini:
query { authors { id, fullName books { id, title authors { id, fullName books { id, title, authors { id, fullName books { id, title authors { ... } } } } } } } }
Jelas bahwa dengan bersarang yang cukup, kueri seperti itu dapat dengan mudah meledakkan server kami. Untuk membatasi kedalaman kueri, kita dapat menggunakan pustaka yang disebut graphql-depth-limit. Setelah kami menginstal, kami dapat menerapkan pembatasan kedalaman saat membuat Server Apollo, seperti ini:
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false, validationRules: [ depthLimit(5) ] }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })
Di sini, kami membatasi kedalaman kueri maksimum hingga lima.
Post Scriptum: Pindah dari REST ke GraphQL Itu Menarik
Dalam tutorial ini, saya mencoba mendemonstrasikan masalah umum yang akan Anda temui saat mulai mengimplementasikan GraphQL API. Namun, beberapa bagiannya memberikan contoh kode yang sangat dangkal dan hanya menggores permukaan masalah yang dibahas, karena ukurannya. Karena itu, untuk melihat contoh kode yang lebih lengkap, silakan merujuk ke repositori Git dari contoh proyek GraphQL API saya: graphql-example.
Pada akhirnya, saya ingin mengatakan bahwa GraphQL adalah teknologi yang sangat menarik. Apakah itu akan menggantikan REST? Tidak ada yang tahu, mungkin besok di dunia TI yang berubah dengan cepat, akan muncul beberapa pendekatan yang lebih baik untuk mengembangkan API, tetapi GraphQL benar-benar termasuk dalam kategori teknologi menarik yang pasti layak untuk dipelajari.