Menu
CoddyTech

Son Tarihler

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

Görevlere bir son tarih atanır ve pano hangilerinin geciktiğini gösterir. Saat içermeyen bir tarih ISO dizgesi olarak gönderilir; 2026-03-15 değerini zod, z.iso.date() ile denetler:

z.iso.date().safeParse('2026-07-01').success   // true
z.iso.date().safeParse('2026-02-30').success   // false: böyle bir gün yok
'2026-03-01' < '2026-03-15'                    // true

ISO tarihleri aynı uzunluktadır ve yılı başa koyar; bu nedenle bunları dizge olarak karşılaştırmak kronolojik sıralamayı verir. /overdue gibi sabit bir yol, yönlendiricide /:id yolundan önce gelmelidir: Express yolları sırayla dener ve /:id herhangi bir sözcükle eşleşir.

challenge icon

Görev

Kolay

Pano, süresi geçmiş görevleri gösterir. Görevlere isteğe bağlı bir son tarih verin ve süresi geçmiş olanların bir listesini ekleyin.

  1. routes/tasks.js içinde Task şemasına z.iso.date() ile doğrulanan isteğe bağlı bir due alanı ekleyin. Bu alan yalnızca YYYY-MM-DD biçiminde yazılmış gerçek tarihleri kabul eder. "2026-03-15" geçer; "15/03/2026", "2026-02-30" ve "tomorrow" ise alışıldık 400 invalid_body hatasını verir. POST isteğinin yeni göreve due değerini kaydettiğinden emin olun.
  2. Yönlendiriciye GET /api/v1/tasks/overdue yolunu ekleyin. Bunu /:id yolunun üstünde tanımlayın; yoksa Express, overdue değerini /:id yoluna bir kimlik olarak gönderir.
  3. Yol, karşılaştırmada kullanılacak tarihi ?today= parametresinden okur; örneğin ?today=2026-03-20. today verilmezse, YYYY-MM-DD biçiminde yazılmış geçerli tarihi kullanır. ?today=soon gibi gerçek bir tarih olmayan today değeri 400 durum kodunu verir:
    { "error": { "code": "invalid_query", "message": "today must be a date like 2026-03-10" } }
  4. Yol, listenin döndürdüğü sayfa nesnesi yerine yalın bir görev dizisi döndürür. Bir görevin süresi şu üç koşulun tümü sağlandığında geçmiş olur: görev tamamlanmamıştır, bir due tarihi vardır ve bu tarih today tarihinden öncedir. Son tarihi today olan bir görevin süresi henüz geçmiş sayılmaz. Diziyi due alanına göre, en erken tarih önce gelecek şekilde sıralayın.

Deneyin: yeni bir Run oturumunda, önce /api/v1/tasks yoluna { "title": "Renew the lease", "due": "2026-03-15" }, ardından { "title": "File the taxes", "due": "2026-03-01" } değerlerini POST edin. Her ikisi de due değerleriyle birlikte 201 döndürür. Ardından GET /api/v1/tasks/overdue?today=2026-03-20 daha erken son tarihi önce döndürür (createdAt zamanlarınız farklı olacaktır):

[
  { "id": 5, "title": "File the taxes", "done": false, "labels": [], "due": "2026-03-01", "createdAt": "2026-10-30T09:15:02.118Z" },
  { "id": 4, "title": "Renew the lease", "done": false, "labels": [], "due": "2026-03-15", "createdAt": "2026-10-30T09:14:40.006Z" }
]

GET /api/v1/tasks/overdue?today=2026-03-15 yalnızca "File the taxes" görevini döndürür. GET /api/v1/tasks/overdue?today=soon ise invalid_query koduyla 400 döndürür.

Kendin dene

import { Router } from 'express';
import { z } from 'zod';
import { config } from '../config.js';
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(),
        labels: z.array(z.string().trim().min(1).max(20)).max(5).optional(),
    })
    .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, label } = 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 (label !== undefined) {
        list = list.filter((t) => t.labels.includes(String(label)));
    }
    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);
    }
    const page = req.query.page === undefined ? 1 : Number(req.query.page);
    const limit = req.query.limit === undefined ? config.pageSize : Number(req.query.limit);
    if (!Number.isInteger(page) || page < 1) {
        return sendError(res, 400, 'invalid_query', 'page must be a whole number from 1');
    }
    if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
        return sendError(res, 400, 'invalid_query', 'limit must be a whole number from 1 to 50');
    }
    const start = (page - 1) * limit;
    res.json({
        items: list.slice(start, start + limit),
        page,
        total: list.length,
        pages: Math.ceil(list.length / limit),
    });
});

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, labels = [] } = result.data;
    if (titleTaken(title)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    const task = { id: nextId(), title, done, labels, createdAt: new Date().toISOString() };
    tasks.push(task);
    res.status(201).location(`/api/v1/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, labels: result.data.labels ?? [], 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;

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

Kendi başına pratik yap: Express playground