Menu
CoddyTech

Sayfalama

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

On bin görevden oluşan bir liste, tek bir yanıt için fazla uzundur. Listeyi sayfalara bölün: istemci ?page=2&limit=20 ister ve bu aralığı, ayrıca “5 sayfadan 2. sayfa” ifadesini göstermek için gereken bilgileri alır:

{ "items": [ ...books 21 to 40... ], "page": 2, "total": 95, "pages": 5 }
const start = (page - 1) * limit;    // page 1, 0'dan başlar
const items = list.slice(start, start + limit);
const pages = Math.ceil(list.length / limit);

total sayısını filtrelerden sonra hesaplayın; çünkü bu sayı, istemcinin istediği listeyi tanımlar. Sorgu dizesinde page yoksa dönüştürme işleminden önce 1, limit içinse 10 kullanın: Number(undefined) değeri NaN olur. Ardından, aralıkta olmayan veya tam sayı olmayan her değeri 400 hatasıyla reddedin. limit için bir üst sınır belirlemek, istemcinin her şeyi bir kerede istemesini önler.

challenge icon

Görev

Kolay

On bin görevden oluşan bir liste tek bir yanıt için çok uzundur. Listeyi sayfalar hâlinde gönderin; böylece istemci "Page 1 of 2" ifadesini gösterebilir.

  1. routes/tasks.js içinde liste rotasının döndürdüğü şeyi değiştirin: dizi yerine bir nesne döndürsün. Başlangıç görevleri için GET /api/tasks?limit=2 şu yanıtı döndürür:
    {
      "items": [
        { "id": 1, "title": "Write the project brief", "done": true, "createdAt": "2026-03-02T09:00:00.000Z" },
        { "id": 2, "title": "Book the meeting room", "done": false, "createdAt": "2026-03-03T10:30:00.000Z" }
      ],
      "page": 1,
      "total": 3,
      "pages": 2
    }
    items bu sayfanın görevlerini tutar, page sayfa numarasıdır, total filtrelerle eşleşen görevlerin sayısıdır ve pages toplam sayfa sayısıdır.
  2. Sorgu dizgesinden page ve limit değerlerini okuyun:
    • page 1'den başlayan bir tam sayıdır. Belirtilmezse 1 kullanın.
    • limit 1 ile 50 arasında bir tam sayıdır. Belirtilmezse 10 kullanın.

    Dönüştürmeden önce varsayılan değeri kullanın; çünkü Number(undefined) değeri NaN olur. Diğer tüm değerler (page=0, page=abc, page=1.5, limit=0, limit=51) durum kodu 400 ile sonuçlanır; hatalı bir limit için mesaj limit must be a whole number from 1 to 50 olur:

    { "error": { "code": "invalid_query", "message": "page must be a whole number from 1" } }
  3. Sayfalama işlemini en son yapın: önce filtreleyip sıralayın, sonra sayfayı ayırın. total, filtrelerden sonra kalan görevleri sayar; bu nedenle ?done=false&limit=1 isteği "total": 2 ve "pages": 2 değerlerini verir.

Bu, yanıtın yapısını değiştirir; dolayısıyla bir dizi bekleyen istemcinin de değişmesi gerekir. Sürümleme dersi bunun nasıl yapılacağını gösterir.

Deneyin: GET /api/tasks?limit=2, "page": 1, "total": 3 ve "pages": 2 değerleriyle 1. ve 2. görevleri döndürür. GET /api/tasks?page=2&limit=2 yalnızca 3. görevi döndürür. Parametresiz GET /api/tasks, "page": 1 ve "pages": 1 değerleriyle 3 görevin tamamını döndürür. GET /api/tasks?page=0, invalid_query koduyla 400 döndürür.

Kendin dene

import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';

const router = Router();

// PUT tarafından gönderildiği biçimiyle eksiksiz bir görev. .strict(), listelemediği alanları reddeder.
const Task = z
    .object({
        title: z.string().trim().min(1).max(100),
        done: z.boolean(),
    })
    .strict();
const NewTask = Task.partial({ done: true });
const TaskChanges = Task.partial();
const SORT_FIELDS = ['title', 'createdAt'];

// Büyük-küçük harf ayrımı gözetmeksizin başka bir görev zaten bu başlığa sahipse true.
const titleTaken = (title, exceptId) =>
    tasks.some((t) => t.id !== exceptId && t.title.toLowerCase() === title.toLowerCase());

router.get('/', (req, res) => {
    const { done, q, sort } = req.query;
    let list = tasks;
    if (done !== undefined) {
        if (done !== 'true' && done !== 'false') {
            return sendError(res, 400, 'invalid_query', 'done must be true or false');
        }
        list = list.filter((t) => t.done === (done === 'true'));
    }
    if (q !== undefined) {
        const words = String(q).toLowerCase();
        list = list.filter((t) => t.title.toLowerCase().includes(words));
    }
    if (sort !== undefined) {
        const field = String(sort).startsWith('-') ? String(sort).slice(1) : String(sort);
        if (!SORT_FIELDS.includes(field)) {
            return sendError(
                res,
                400,
                'invalid_query',
                'sort must be title or createdAt, with - in front to reverse',
            );
        }
        const direction = String(sort).startsWith('-') ? -1 : 1;
        list = list.toSorted((a, b) => a[field].localeCompare(b[field]) * direction);
    }
    res.json(list);
});

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

router.post('/', (req, res) => {
    const result = NewTask.safeParse(req.body);
    if (!result.success) {
        return sendInvalid(res, result.error);
    }
    const { title, done = false } = result.data;
    if (titleTaken(title)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    const task = { id: nextId(), title, done, 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 sendError(res, 404, 'not_found', 'Task not found');
    }
    const result = TaskChanges.safeParse(req.body);
    if (!result.success) {
        return sendInvalid(res, result.error);
    }
    if (result.data.title !== undefined && titleTaken(result.data.title, task.id)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    Object.assign(task, result.data);
    res.json(task);
});

router.put('/:id', (req, res) => {
    const index = tasks.findIndex((t) => t.id === Number(req.params.id));
    if (index === -1) {
        return sendError(res, 404, 'not_found', 'Task not found');
    }
    const result = Task.safeParse(req.body);
    if (!result.success) {
        return sendInvalid(res, result.error);
    }
    const { id, createdAt } = tasks[index];
    if (titleTaken(result.data.title, id)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    tasks[index] = { id, ...result.data, 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 sendError(res, 404, 'not_found', 'Task not found');
    }
    tasks.splice(index, 1);
    res.status(204).end();
});

export default router;
quiz iconKendini test et

Bu ders kısa bir quiz içerir. Soruları yanıtlamak ve ilerlemeni kaydetmek için derse başla.

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

Kendi başına pratik yap: Express playground