Menu
CoddyTech

zod ile Şemalar

Coddy'nin Node.js Journey'sinin REST API'leri bölümünün bir parçası. Ders 11 / 25.

Elle yapılan kontroller her alan ve her uç noktayla birlikte artar. Bir zod şeması biçimi bir kez tanımlar ve safeParse bir değeri bu şemaya göre sınar:

import { z } from 'zod';

const Event = z.object({
    name: z.string().trim().min(1).max(80),
    seats: z.number().int().min(1),
}).strict();

const result = Event.safeParse(req.body);
if (!result.success) {
    return res.status(400).json({ error: z.flattenError(result.error) });
}
// result.data, name alanındaki boşlukları kırpılmış doğrulanmış body'yi içerir

.strict(), şemada listelenmeyen alanları reddeder. Event.partial(), tüm alanları isteğe bağlı hâle getirir; PATCH için gereken de budur. Event.partial({ seats: true }) yalnızca seats alanını isteğe bağlı hâle getirir ve const { seats = 1 } = result.data, bu alan eksik olduğunda 1 değerini kullanır. z.flattenError(result.error), sorunları { formErrors, fieldErrors } biçimine dönüştürür: formErrors, bilinmeyen bir alan gibi tüm gövdeyle ilgili sorunları içerir; fieldErrors ise her alana ait sorunları içerir. Event.parse aynı kontrolü yapar ancak geçersiz bir değerle karşılaştığında hata fırlatır; bir uç noktada fırlatılan hata 500 yanıtına dönüşür.

challenge icon

Görev

Kolay

Bu adım, Validating Input dersindeki proje dosyalarınız yerine kurs için hazırlanmış görev takip API projesini açar (bunların arasında routes/tasks.js de vardır). Önceki çalışmalarınız kendi dersinde kalır. routes/tasks.js dosyasını düzenlemeden önce sağlanan dosyaları okuyun. Bu kopyada görevler için CRUD rotaları ve elle yazılmış girdi doğrulaması bulunur.

Elle yazdığınız kontroller, her alan ve her rotayla birlikte artar. Bir görevi bir kez zod şemasıyla tanımlayın ve gövde alan üç rotanın da bu şemayı kullanmasını sağlayın.

  1. routes/tasks.js içinde, z öğesini zod içinden içe aktarın ve Task adlı bir şema yazın:
    • title: bir dize; baştaki ve sondaki boşluklar kaldırıldıktan sonra 1 ile 100 karakter arasında olmalıdır.
    • done: true veya false (z.boolean()).
    • Şema katıdır; bu nedenle listede bulunmayan color gibi bir alan reddedilir.

    Sağlanan titleError yardımcı işlevini silin (kursun Validating Input dersindeki doğrulama sürümünü).

  2. Task öğesinden .partial() kullanarak iki sürüm oluşturun. POST sürümünde yalnızca done isteğe bağlı olur. PATCH sürümünde her iki alan da isteğe bağlı olur. PUT, Task öğesinin kendisini kullanır; bu nedenle her iki alanı da gerektirir.
  3. POST, PATCH ve PUT içinde req.body değerini safeParse ile kontrol edin. Kontrol başarısız olursa 400 durum koduyla yanıt verin ve zod'un sorunlar listesini error altında, z.flattenError(result.error) ile oluşturulmuş şekilde gönderin. { "title": 123 } için sonuç şöyle görünür:
    { "error": { "formErrors": [], "fieldErrors": { "title": ["Invalid input: expected string, received number"] } } }
  4. Kontrol başarılı olduğunda req.body yerine result.data içinden kaydedin: bu değer, baştaki ve sondaki boşlukları kaldırılmış başlığı içerir. POST işleminde, done içermeyen bir gövde görevi "done": false ile oluşturur (eksik alan result.data içinde undefined olur ve ?? bir yedek değer sağlar). PUT, kayıtlı görevdeki id ve createdAt değerlerini korur.

Deneyin: { "title": " Order new chairs ", "color": "red" } değerini /api/tasks adresine POST edin. 400 alırsınız ve formErrors, Unrecognized key: "color" der. color olmadan yeniden gönderin: "title": "Order new chairs" (boşluksuz) ve "done": false ile 201 alırsınız. { "done": "yes" } ile PATCH /api/tasks/2 isteği, fieldErrors içinde done girdisiyle 400 döndürür. { "title": "Book the big room", "done": false } ile PUT /api/tasks/2 isteği, görev 2'nin eski createdAt değeriyle 200 döndürür.

Kendin dene

import { Router } from 'express';
import { nextId, tasks } from '../store.js';

const router = Router();

// title'ın saklanamama nedeni; saklanabiliyorsa null.
const titleError = (title) => {
    if (typeof title !== 'string' || title.trim() === '' || title.length > 100) {
        return 'title must be a string of 1 to 100 characters';
    }
    return null;
};

router.get('/', (req, res) => {
    res.json(tasks);
});

router.get('/:id', (req, res) => {
    const task = tasks.find((t) => t.id === Number(req.params.id));
    if (!task) {
        return res.status(404).json({ error: 'Task not found' });
    }
    res.json(task);
});

router.post('/', (req, res) => {
    const { title } = req.body ?? {};
    const problem = titleError(title);
    if (problem) {
        return res.status(400).json({ error: problem });
    }
    const task = { id: nextId(), title, done: false, createdAt: new Date().toISOString() };
    tasks.push(task);
    res.status(201).location(`/api/tasks/${task.id}`).json(task);
});

router.patch('/:id', (req, res) => {
    const task = tasks.find((t) => t.id === Number(req.params.id));
    if (!task) {
        return res.status(404).json({ error: 'Task not found' });
    }
    const { title, done } = req.body ?? {};
    if (title !== undefined && titleError(title)) {
        return res.status(400).json({ error: titleError(title) });
    }
    if (done !== undefined && typeof done !== 'boolean') {
        return res.status(400).json({ error: 'done must be true or false' });
    }
    if (title !== undefined) task.title = title;
    if (done !== undefined) task.done = done;
    res.json(task);
});

router.put('/:id', (req, res) => {
    const index = tasks.findIndex((t) => t.id === Number(req.params.id));
    if (index === -1) {
        return res.status(404).json({ error: 'Task not found' });
    }
    const { title, done } = req.body ?? {};
    if (title === undefined || done === undefined) {
        return res.status(400).json({ error: 'PUT needs every field: title and done' });
    }
    const { id, createdAt } = tasks[index];
    tasks[index] = { id, title, done, createdAt };
    res.json(tasks[index]);
});

router.delete('/:id', (req, res) => {
    const index = tasks.findIndex((t) => t.id === Number(req.params.id));
    if (index === -1) {
        return res.status(404).json({ error: 'Task not found' });
    }
    tasks.splice(index, 1);
    res.status(204).end();
});

export default router;

REST API'leri bölümündeki tüm dersler

Kendi başına pratik yap: Express playground