Episode 10 memperketat keamanan data masuk: validasi otomatis di level schema, integrasi Zod untuk validasi argumen dan input di resolver, teknik sanitasi untuk mencegah XSS serta SQL dan NoSQL injection, custom scalar dengan validasi, hingga desain error response yang ramah client.

Data yang masuk ke API kalian adalah permukaan serangan paling besar. Episode 10 membahas validasi dan sanitasi input — lapisan pertahanan pertama yang memastikan hanya data yang benar dan aman yang sampai ke resolver dan database.
Kita akan memanfaatkan validasi otomatis yang sudah diberikan schema, menambahkan validasi bisnis dengan Zod, menerapkan teknik sanitasi untuk mencegah XSS dan injection, membuat custom scalar dengan validasi bawaan, dan merancang format error response yang konsisten dan ramah bagi client.
Kabar baiknya, GraphQL sudah memberikan validasi struktural secara gratis. Sebelum resolver dieksekusi, GraphQL memeriksa:
Int tidak menerima string).input RegisterInput {
email: String!
age: Int!
role: UserRole!
}
enum UserRole {
ADMIN
MEMBER
}Coba kirim age: "dua puluh" atau role: "SUPER" — GraphQL menolaknya sebelum resolver dijalankan. Ini fondasi yang kuat, tapi belum cukup: schema tidak bisa memvalidasi logika bisnis seperti "email harus mengandung titik" atau "password minimal 8 karakter".
Validasi format khusus bisa disisipkan ke dalam custom scalar:
import { GraphQLScalarType, Kind } from "graphql";
const Email = new GraphQLScalarType({
name: "Email",
serialize: (value) => String(value),
parseValue: (value) => {
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) {
throw new Error("Format email tidak valid");
}
return value;
},
parseLiteral: (ast) => {
if (ast.kind !== Kind.STRING) throw new Error("Email harus berupa string");
return ast.value;
},
});Custom scalar Email ini menolak string yang bukan email pada tahap parsing. Perpustakaan graphql-scalars menyediakan banyak scalar siap pakai seperti DateTime, Email, dan JSON dengan validasi bawaan.
Validasi struktural dari schema menangani tipe, tapi validasi bisnis seperti panjang string, pola, atau relasi antar field butuh library. Zod adalah pilihan modern yang populer karena type inference-nya mulus dengan TypeScript:
npm install zodimport { z } from "zod";
const registerInputSchema = z.object({
email: z.string().email(),
password: z.string().min(8).max(72),
age: z.number().int().positive().min(13),
});Perhatikan bahwa skema Zod menghasilkan tipe TypeScript secara otomatis — z.infer<typeof registerInputSchema> memberi kalian tipe yang aman tanpa menulis dua kali. Ini membuat validasi dan type safety menyatu.
Ada dua lapisan yang perlu kalian jaga konsistensinya. Lapisan pertama adalah validasi di resolver sebelum logika bisnis dijalankan:
async function register(_, args, ctx) {
const parsed = registerInputSchema.safeParse(args.input);
if (!parsed.success) {
return {
ok: false,
errors: parsed.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
})),
};
}
return ctx.userService.register(parsed.data);
}Perhatikan pola safeParse yang tidak melempar exception, melainkan mengembalikan hasil yang bisa diperiksa. Ini memungkinkan mengumpulkan semua error sekaligus dan menampilkannya per field — jauh lebih ramah client dibanding melempar error pertama yang ditemukan.
Untuk aturan yang melibatkan database — misalnya "username harus unik" — buat fungsi validasi khusus dan panggil sebelum operasi disimpan. Pisahkan fungsi-fungsi ini agar bisa dipakai ulang dan dites secara terpisah di episode 21.
Sanitasi menetralkan input berbahaya sebelum data disimpan atau dirender. Dua ancaman utama:
const post = await ctx.prisma.post.create({
data: {
title: args.input.title,
body: args.input.body,
},
});Prisma dan ORM modern selalu memakai parameterized queries, sehingga nilai pengguna tidak pernah disisipkan langsung ke SQL. Untuk MongoDB, hindari operator $where dan $ berbahaya yang bisa memicu NoSQL injection. Untuk teks yang dirender di browser, sanitasi dengan library seperti DOMPurify di sisi client.
Batasi panjang input untuk mencegah penyalahgunaan: String schema tidak membatasi panjang. Dengan Zod, tambahkan .max(5000) pada field deskripsi. Kombinasikan dengan batas ukuran request di HTTP server untuk melindungi dari payload raksasa.
Desain format error sejak awal agar client bisa menanganinya secara terprogram:
{
"errors": [
{
"field": "email",
"message": "Format email tidak valid"
}
]
}Gunakan payload pattern dengan errors per field seperti yang diperkenalkan di episode 5, dan definisikan kode error yang jelas: VALIDATION_ERROR, NOT_FOUND, UNAUTHORIZED. Kategorisasi ini akan menjadi dasar desain error handling menyeluruh di episode 11.
Inti yang harus dibawa pulang:
Di episode 11 selanjutnya kalian akan mempelajari error handling dan custom errors — struktur error GraphQL dengan extensions, custom error classes, pola throwing versus returning error, error masking untuk keamanan, serta union types untuk type-safe error handling. Resolver kalian akan gagal dengan elegan, bukan dengan membingungkan client!