Tenggat Waktu
Termasuk bagian REST API dalam perjalanan Node.js di Coddy. Pelajaran 25 dari 25.
Tugas memiliki tanggal tenggat, dan papan menampilkan tugas mana yang terlambat. Tanggal tanpa waktu ditulis sebagai string ISO, 2026-03-15, yang diperiksa oleh zod dengan z.iso.date():
z.iso.date().safeParse('2026-07-01').success // true
z.iso.date().safeParse('2026-02-30').success // false: tanggal tersebut tidak ada
'2026-03-01' < '2026-03-15' // trueTanggal ISO memiliki panjang yang sama dan menempatkan tahun di awal, jadi membandingkannya sebagai string menghasilkan urutan waktu. Jalur tetap seperti /overdue harus ditulis sebelum /:id di router: Express mencoba rute secara berurutan, dan /:id cocok dengan kata apa pun.
Tantangan
MudahBoard menampilkan tugas yang terlambat. Tambahkan tanggal tenggat opsional pada tugas, lalu tambahkan daftar tugas yang terlambat.
- Di
routes/tasks.js, tambahkan fielddueopsional ke skemaTask, dengan validasiz.iso.date(). Skema ini hanya menerima tanggal yang valid dengan formatYYYY-MM-DD."2026-03-15"lolos, sedangkan"15/03/2026","2026-02-30"dan"tomorrow"menghasilkan errorinvalid_body400 seperti biasanya. Pastikan POST menyimpanduepada tugas baru. - Tambahkan
GET /api/v1/tasks/overdueke router. Deklarasikan rute ini di atas rute/:id, atau Express akan mengirimoverdueke/:idsebagai id. - Rute ini membaca tanggal pembanding dari
?today=, misalnya?today=2026-03-20. Jikatodaytidak ada, rute menggunakan tanggal saat ini dengan formatYYYY-MM-DD. Nilaitodayyang bukan tanggal valid, seperti?today=soon, menghasilkan status 400:{ "error": { "code": "invalid_query", "message": "today must be a date like 2026-03-10" } } - Rute ini mengembalikan array tugas biasa, bukan object halaman yang dikembalikan oleh daftar tugas. Tugas terlambat jika memenuhi ketiga kondisi ini: tugas belum selesai, memiliki tanggal
due, dan tanggal tersebut sebelumtoday. Tugas yang tanggal tenggatnya tepat padatodaybelum terlambat. Urutkan array berdasarkandue, dari tanggal paling awal.
Coba: setelah menjalankan Run dengan data baru, kirim POST { "title": "Renew the lease", "due": "2026-03-15" }, lalu { "title": "File the taxes", "due": "2026-03-01" } ke /api/v1/tasks. Keduanya mengembalikan 201 beserta due-nya. Lalu GET /api/v1/tasks/overdue?today=2026-03-20 mengembalikan tanggal tenggat yang lebih awal terlebih dahulu (waktu createdAt-mu akan berbeda):
[
{ "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 hanya mengembalikan "File the taxes". GET /api/v1/tasks/overdue?today=soon mengembalikan 400 dengan kode invalid_query.
Coba sendiri
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();
// Task lengkap, seperti yang dikirim oleh PUT. .strict() menolak field yang tidak tercantum.
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'];
// Bernilai true jika task lain sudah memiliki judul ini, tanpa membedakan huruf besar dan kecil.
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;
Semua pelajaran di REST API
1Sumber Daya dan Kata Kerja
Sumber Daya dan URLMembuat dengan POST4Peramban, Versi, dan Konfigurasi
Apa Itu OriginMengizinkan Origin LainCORSHalaman yang Memanggil APILatihan sendiri: Playground Express