Vezérlőpult
9. REST API CRUD-dal · Express alapok

Express alapok · 9. tananyag · a blokk záró fejezete

REST API CRUD-dal

Itt áll össze egyetlen működő alkalmazássá minden, amit eddig tanultunk. Nem lesz benne új Express-fogalom — helyette végigcsináljuk a teljes utat a végponttérképtől a kipróbálásig, adatbázis nélkül, memóriában.

4 tanóraHat végpontPUT vs. PATCH

A fejezet végén tudni fogod

1A szerződés

Mielőtt egy sort is írnánk, rögzítjük, mit ígér az API. Ez a „szerződés”, amiről az 1. blokk 2. fejezetében volt szó.

A KOLLEKCIÓ /api/products GET terméklista szűréssel és lapozással 200 ?categoryId · ?q · ?page · ?limit POST új termék létrehozása 201 + Location fejléc · hibánál 400 EGY ELEM /api/products/:id GET egy termék 200 PUT TELJES felülírás 200 PATCH RÉSZLEGES módosítás 200 DELETE törlés 204 Mind a négy: hibás azonosítónál 400, nem létezőnél 404. Az ellenőrzést közös middleware végzi — nem négyszer írjuk le. CRUD = Create · Read · Update · Delete Négy művelet, hat végpont — az olvasás és a módosítás kétféle.
1. ábra — A végponttérkép. Ez a fejezet „térképe”, érdemes visszalapozni rá.

Végponttérkép

MetódusÚtvonalMit csinál?SikerHiba
GET/api/productsTerméklista szűréssel és lapozással200—
GET/api/products/:idEgy termék200400, 404
POST/api/productsÚj termék201 + Location400
PUT/api/products/:idTeljes felülírás200400, 404
PATCH/api/products/:idRészleges módosítás200400, 404
DELETE/api/products/:idTörlés204400, 404

A termék alakja

{ "id": 1, "nev": "Billentyűzet", "ar": 12990, "keszlet": 8, "categoryId": 3 }
MezőTípusKötelező?Szabály
idszám—A szerver adja, a kliens soha nem küldi
nevszöveg✅ igenNem lehet üres; a szóközöket levágjuk
arszám✅ igenNem lehet negatív
keszletegész számnemNem lehet negatív; alapértéke 0
categoryIdegész számnemPozitív; alapértéke null

A listavégpont query paraméterei

ParaméterPéldaAlapértékMit csinál?
categoryId?categoryId=3—Szűrés kategóriára
q?q=egér—Keresés a névben, kis- és nagybetűtől függetlenül
page?page=21Hányadik oldal
limit?limit=1020Hány elem egy oldalon

A válaszformátum

Lista — az adat mellé a lapozáshoz szükséges információ is kell:

{ "elemek": [ … ], "osszesTalalat": 3, "oldal": 1, "meret": 20 }

Hiba — mindig ugyanaz a szerkezet (5. és 8. fejezet):

{ "error": { "message": "Nincs ilyen termék.", "code": "TERMEK_NEM_TALALHATO" } }
MIÉRT NEM CSAK EGY TÖMB A LISTA?

Mert a lapozáshoz a kliensnek tudnia kell, hány találat van összesen — enélkül nem tud lapozósávot rajzolni. Két bevett megoldás van: a becsomagolt objektum (ezt választjuk), vagy az X-Total-Count fejléc. Mi mindkettőt megadjuk, mert a fejléc olcsó, és van kliens, amelyik azt várja.

Melyik végpont és milyen státusz?

Nyolc helyzet a webshopból. Válaszd ki, melyik kérés tartozik hozzá — a metódust és az útvonalat együtt.

1 / 8Helyes: 0
…

2A projekt felállítása

mkdir webshop-api
cd webshop-api
npm init -y
npm install express

Egészítsd ki a package.json fájlt:

{
  "type": "module",
  "scripts": {
    "dev": "node --watch --env-file=.env index.js",
    "start": "node index.js"
  }
}

A mappaszerkezet a 7. fejezetből:

webshop-api/
  index.js
  src/
    app.js
    routes/termek.routes.js
    controllers/termek.controller.js
    services/termek.service.js
    middlewares/
      ervenyes-azonosito.js
      nem-talalhato.js
      hibakezelo.js
    utils/
      HttpHiba.js
      validalas.js
  keresek.http

3Az adatréteg

A service a memóriában tárolt tömböt kezeli. Nincs benne req, res, se státuszkód — ez a 7. fejezet szabálya.

// src/services/termek.service.js
let termekek = [
  { id: 1, nev: "Billentyűzet", ar: 12990, keszlet: 8,  categoryId: 3 },
  { id: 2, nev: "Egér",         ar: 4990,  keszlet: 25, categoryId: 3 },
  { id: 3, nev: "Monitor",      ar: 89990, keszlet: 0,  categoryId: 1 }
];
let kovetkezoId = 4;

export function osszes({ categoryId, q, oldal = 1, meret = 20 } = {}) {
  let talalatok = termekek;

  if (categoryId !== undefined) {
    talalatok = talalatok.filter(t => t.categoryId === categoryId);
  }
  if (q) {
    const kis = q.toLowerCase();
    talalatok = talalatok.filter(t => t.nev.toLowerCase().includes(kis));
  }

  const osszesTalalat = talalatok.length;      // a SZŰRT találatok száma — lapozás ELŐTT
  const kezdet = (oldal - 1) * meret;

  return { elemek: talalatok.slice(kezdet, kezdet + meret), osszesTalalat };
}

export function azonositoval(id) {
  return termekek.find(t => t.id === id) ?? null;
}

export function letrehoz(adat) {
  const termek = {
    id: kovetkezoId++,
    nev: adat.nev,
    ar: adat.ar,
    keszlet: adat.keszlet ?? 0,
    categoryId: adat.categoryId ?? null
  };
  termekek.push(termek);
  return termek;
}

export function felulir(id, adat) {
  const index = termekek.findIndex(t => t.id === id);
  if (index === -1) return null;

  termekek[index] = {                          // TELJES csere — a hiányzó mezők alapértékre állnak
    id,
    nev: adat.nev,
    ar: adat.ar,
    keszlet: adat.keszlet ?? 0,
    categoryId: adat.categoryId ?? null
  };
  return termekek[index];
}

export function modosit(id, valtozasok) {
  const termek = azonositoval(id);
  if (!termek) return null;

  Object.assign(termek, valtozasok);           // csak a megadott mezőket írja felül
  return termek;
}

export function torol(id) {
  const index = termekek.findIndex(t => t.id === id);
  if (index === -1) return false;

  termekek.splice(index, 1);
  return true;
}
FüggvényMit ad vissza, ha nincs találat?Miért így?
azonositovalnull„Megkerestem, nincs ilyen”
felulir, modositnullUgyanaz — a vezérlő csinál belőle 404-et
torolfalseItt nincs mit visszaadni, csak a siker ténye
A let termekek ÉS AZ id KIOSZTÁSA IDEIGLENES

Adatbázisnál a kovetkezoId számláló eltűnik: az azonosítót maga az adatbázis adja. A filter-ekből WHERE feltétel lesz, a slice-ból pedig LIMIT/OFFSET. A függvények neve és a visszaadott érték viszont változatlan marad — ezért nem kell hozzányúlni a vezérlőhöz.

4A validáció

A bejövő adatot egy helyen ellenőrizzük, és HttpHiba-t dobunk (8. fejezet):

// src/utils/validalas.js
import { HttpHiba } from "./HttpHiba.js";

export function termekAdatok(torzs, { reszleges = false } = {}) {
  if (typeof torzs !== "object" || torzs === null || Array.isArray(torzs)) {
    throw new HttpHiba(400, "A kérés törzse hiányzik vagy nem objektum.", "HIANYZO_TORZS");
  }

  const eredmeny = {};

  // nev — kötelező, kivéve részleges módosításnál
  if (torzs.nev !== undefined) {
    if (typeof torzs.nev !== "string" || torzs.nev.trim() === "") {
      throw new HttpHiba(400, "A név nem lehet üres.", "ERVENYTELEN_NEV");
    }
    eredmeny.nev = torzs.nev.trim();
  } else if (!reszleges) {
    throw new HttpHiba(400, "A név megadása kötelező.", "HIANYZO_MEZO");
  }

  // ar — kötelező, kivéve részleges módosításnál
  if (torzs.ar !== undefined) {
    if (typeof torzs.ar !== "number" || !Number.isFinite(torzs.ar) || torzs.ar < 0) {
      throw new HttpHiba(400, "Az ár csak nem negatív szám lehet.", "ERVENYTELEN_AR");
    }
    eredmeny.ar = torzs.ar;
  } else if (!reszleges) {
    throw new HttpHiba(400, "Az ár megadása kötelező.", "HIANYZO_MEZO");
  }

  // keszlet — mindig elhagyható
  if (torzs.keszlet !== undefined) {
    if (!Number.isInteger(torzs.keszlet) || torzs.keszlet < 0) {
      throw new HttpHiba(400, "A készlet csak nem negatív egész szám lehet.", "ERVENYTELEN_KESZLET");
    }
    eredmeny.keszlet = torzs.keszlet;
  }

  // categoryId — mindig elhagyható
  if (torzs.categoryId !== undefined) {
    if (!Number.isInteger(torzs.categoryId) || torzs.categoryId < 1) {
      throw new HttpHiba(400, "A kategória azonosítója csak pozitív egész szám lehet.", "ERVENYTELEN_KATEGORIA");
    }
    eredmeny.categoryId = torzs.categoryId;
  }

  if (reszleges && Object.keys(eredmeny).length === 0) {
    throw new HttpHiba(400, "Legalább egy módosítandó mezőt meg kell adni.", "NINCS_MODOSITAS");
  }

  return eredmeny;
}

Amit ez a függvény megold

FeladatHogyan?
Ismétlés elkerüléseEgy függvény szolgálja ki a POST, PUT és PATCH végpontot
Kötelező vs. elhagyhatóA reszleges kapcsoló dönti el: PATCH-nél semmi nem kötelező
TisztításA nev értékéről levágja a felesleges szóközöket (trim)
SzűrésCsak az ismert mezőket adja vissza — az id vagy bármi más ismeretlen kimarad
A LEGFONTOSABB SOR AZ UTOLSÓ: return eredmeny

Nem a nyers req.body-t adjuk tovább, hanem egy általunk összeállított objektumot. Így hiába küld a kliens {"nev":"X","ar":1,"id":9999,"admin":true} törzset, az id és az admin mező be sem kerül az adatba. Ez a legegyszerűbb védekezés az ellen, hogy valaki olyan mezőt írjon felül, amit nem szabadna.

MIÉRT NEM VALIDÁCIÓS KÖNYVTÁR?

Mert így látod, mit is csinál egy ilyen könyvtár. A 3. blokkban a Zod veszi át ezt a szerepet: ott ugyanez a szabályrendszer néhány sor lesz, és a hibaüzeneteket is megkapod hozzá. Előbb viszont érdemes kézzel megírni egyszer.

5Az azonosító-ellenőrző middleware

A négy /:id végpont mindegyikének ugyanaz az első dolga. A 6. fejezetből tudjuk, hogy ez middleware-be való:

// src/middlewares/ervenyes-azonosito.js
import { HttpHiba } from "../utils/HttpHiba.js";

export function ervenyesAzonosito(req, res, next) {
  const id = Number(req.params.id);

  if (!Number.isInteger(id) || id < 1) {
    return next(new HttpHiba(400, "Az azonosító csak pozitív egész szám lehet.", "ERVENYTELEN_ID"));
  }

  req.termekId = id;          // a már átalakított érték a vezérlőnek
  next();
}

Az eredmények:

GET /api/products/alma   →   400   {"error":{"message":"Az azonosító csak pozitív egész szám lehet.", …}}
GET /api/products/-3     →   400   ugyanaz
GET /api/products/1      →   200   a termék

A Number.isInteger egyszerre zárja ki a NaN, a tizedes tört és a -3 esetét — a Number("alma") ugyanis NaN, a Number.isInteger(NaN) pedig hamis.

6A vezérlő: hat függvény

// src/controllers/termek.controller.js
import * as szolgaltatas from "../services/termek.service.js";
import { termekAdatok } from "../utils/validalas.js";
import { HttpHiba } from "../utils/HttpHiba.js";

export function listaz(req, res) {
  const oldal = Number(req.query.page) || 1;
  const meret = Number(req.query.limit) || 20;
  const categoryId = req.query.categoryId ? Number(req.query.categoryId) : undefined;

  const { elemek, osszesTalalat } = szolgaltatas.osszes({ categoryId, q: req.query.q, oldal, meret });

  res.set("X-Total-Count", String(osszesTalalat));
  res.json({ elemek, osszesTalalat, oldal, meret });
}

export function egy(req, res) {
  const termek = szolgaltatas.azonositoval(req.termekId);
  if (!termek) throw new HttpHiba(404, "Nincs ilyen termék.", "TERMEK_NEM_TALALHATO");
  res.json(termek);
}

export function letrehoz(req, res) {
  const adat = termekAdatok(req.body);
  const termek = szolgaltatas.letrehoz(adat);
  res.location(`/api/products/${termek.id}`).status(201).json(termek);
}

export function felulir(req, res) {
  const adat = termekAdatok(req.body);                          // minden mező kötelező
  const termek = szolgaltatas.felulir(req.termekId, adat);
  if (!termek) throw new HttpHiba(404, "Nincs ilyen termék.", "TERMEK_NEM_TALALHATO");
  res.json(termek);
}

export function modosit(req, res) {
  const valtozasok = termekAdatok(req.body, { reszleges: true });   // csak amit küldtek
  const termek = szolgaltatas.modosit(req.termekId, valtozasok);
  if (!termek) throw new HttpHiba(404, "Nincs ilyen termék.", "TERMEK_NEM_TALALHATO");
  res.json(termek);
}

export function torol(req, res) {
  const sikerult = szolgaltatas.torol(req.termekId);
  if (!sikerult) throw new HttpHiba(404, "Nincs ilyen termék.", "TERMEK_NEM_TALALHATO");
  res.sendStatus(204);
}

Figyeld meg, milyen rövidek. Ez azért lehetséges, mert:

Amit nem kell megírniHol van?
A return a korai válasz eléthrow — a függvény magától megszakad (8. fejezet)
A hibaválasz összerakásaA központi hibakezelőben (8. fejezet)
Az azonosító átalakítása és ellenőrzéseA middleware-ben (5. szakasz)
A törzs ellenőrzéseA termekAdatok függvényben (4. szakasz)
Az adatkezelésA service-ben (3. szakasz)
A res.sendStatus(204) A TÖRLÉSNÉL

A 204 No Content azt jelenti: „sikerült, és nincs mit visszaadnom”. Törzset nem küldünk — ezért nem res.json()-t használunk (3. fejezet).

7PUT vs. PATCH

Ez a fejezet legfontosabb REST-fogalma, és a leggyakoribb félreértés.

KIINDULÁS { "id": 1, "nev": "Billentyűzet", "ar": 12990, "keszlet": 8, "categoryId": 3 } PUT „így nézzen ki az erőforrás” { "nev": "Mechanikus billentyűzet", "ar": 24990, "categoryId": 3 } { "id": 1, "nev": "Mechanikus …", "ar": 24990, "keszlet": 0, "categoryId": 3 } A keszlet 8-ról 0-ra változott! PATCH „ezt változtasd meg rajta” { "ar": 22990 } { "id": 1, "nev": "Billentyűzet", "ar": 22990, "keszlet": 8, "categoryId": 3 } Csak az ár változott.
2. ábra — Tesztelt eredmények. A PUT alapértékre állítja azt, amit nem küldtél.
PUTPATCH
Mit jelent?„Így nézzen ki az erőforrás”„Ezt változtasd meg rajta”
A törzsA teljes objektumCsak a módosítandó mezők
Kötelező mezőknev, aregyik sem, de legalább egy kell
A hiányzó mezőkalapértékre állnakváltozatlanok maradnak
KódbantermekAdatok(req.body)termekAdatok(req.body, { reszleges: true })
A service-bentermekek[index] = { … } — csereObject.assign(termek, …) — összefésülés
A GYAKORLATBAN A PATCH-ET HASZNÁLJUK GYAKRABBAN

Egy űrlapon ritkán szerkeszted az összes mezőt egyszerre, és a PUT könnyen letöröl olyan adatot, amihez hozzá sem nyúltál. A PUT ott jó, ahol tényleg a teljes objektumot küldi a kliens.

EMLÉKEZTETŐ AZ 1. BLOKK 2. FEJEZETÉBŐL

A PUT idempotens: ugyanazt a kérést kétszer elküldve ugyanaz az eredmény. A PATCH nem feltétlenül az — egy „növeld eggyel a készletet” típusú módosítás kétszer futtatva mást ad.

Mi lesz a termék a kérés után?

Öt kérés, mindig ugyanabból a kiindulási termékből. Válaszd ki az eredményt!

1 / 5Helyes: 0
…

8A router és az app.js

// src/routes/termek.routes.js
import express from "express";
import * as vezerlo from "../controllers/termek.controller.js";
import { ervenyesAzonosito } from "../middlewares/ervenyes-azonosito.js";

const router = express.Router();

router.get("/", vezerlo.listaz);
router.post("/", vezerlo.letrehoz);

router.get("/:id",    ervenyesAzonosito, vezerlo.egy);
router.put("/:id",    ervenyesAzonosito, vezerlo.felulir);
router.patch("/:id",  ervenyesAzonosito, vezerlo.modosit);
router.delete("/:id", ervenyesAzonosito, vezerlo.torol);

export default router;
MIÉRT NEM router.route("/:id").all(ervenyesAzonosito)?

Első ránézésre rövidebb lenne — de kipróbálva elrontja az OPTIONS kezelését. Az .all() ugyanis az OPTIONS kérésre is lefut, az Express pedig ilyenkor már nem generálja le magától az Allow fejlécet:

.all()-lal:      OPTIONS /api/products/1   →   404
metódusonként:   OPTIONS /api/products/1   →   200, Allow: DELETE, GET, HEAD, PATCH, PUT

A metódusonkénti felsorolás négy karakterrel hosszabb, cserébe helyes marad az automatikus OPTIONS- és HEAD-kezelés (4. fejezet).

HA A cors() IS FUT A LÁNCBAN

A fenti eredmény az ebben a fejezetben épülő app.js-re vonatkozik. Ha kiegészíted a 6. fejezet middleware-eivel, a cors() maga válaszol az OPTIONS-kérésekre — 204-gyel, Allow fejléc nélkül —, még mielőtt a router szóhoz jutna:

cors() nélkül:   OPTIONS /api/products/1   →   200, Allow: DELETE, GET, HEAD, PATCH, PUT
cors()-szal:     OPTIONS /api/products/1   →   204, Allow fejléc nélkül

Ez nem hiba: a CORS-szabvány így írja elő az előkérés (preflight) kezelését — a böngészőnek nem az Allow, hanem az Access-Control-Allow-Methods fejléc kell, amit a cors() küld. Az .all() csapdája viszont ettől függetlenül valódi: cors() nélkül tényleg 404-et adna.

// src/app.js
import express from "express";
import termekRouter from "./routes/termek.routes.js";
import { nemTalalhato } from "./middlewares/nem-talalhato.js";
import { hibakezelo } from "./middlewares/hibakezelo.js";

const app = express();

app.use(express.json({ limit: "100kb" }));      // 1) törzs-feldolgozás
app.use("/api/products", termekRouter);         // 2) útvonalak
app.use(nemTalalhato);                          // 3) 404
app.use(hibakezelo);                            // 4) hibakezelő — legvégül

export default app;

A hibakezelő a törzs-feldolgozó hibáit is a saját formátumunkra fordítja (5. és 8. fejezet):

// src/middlewares/hibakezelo.js — az eleje
export function hibakezelo(err, req, res, next) {
  if (err.type === "entity.parse.failed") {
    return res.status(400).json({
      error: { message: "A kérés törzse nem érvényes JSON.", code: "ERVENYTELEN_JSON" }
    });
  }
  if (err.type === "entity.too.large") {
    return res.status(413).json({
      error: { message: "A kérés törzse túl nagy.", code: "TUL_NAGY_TORZS" }
    });
  }
  // …a 8. fejezet szerinti általános rész…
}

9Kipróbálás

A keresek.http fájl (REST Client, 1. blokk 5. fejezet):

@alap = http://localhost:3000

### Összes termék
GET {{alap}}/api/products

### Szűrés kategóriára
GET {{alap}}/api/products?categoryId=3

### Új termék
POST {{alap}}/api/products
Content-Type: application/json

{ "nev": "Webkamera", "ar": 19990, "categoryId": 2 }

### Részleges módosítás
PATCH {{alap}}/api/products/1
Content-Type: application/json

{ "ar": 22990 }

### Törlés
DELETE {{alap}}/api/products/2

Próbáld ki a teljes API-t

Kattints egy kérésre, és nézd meg a valódi választ. Minden itt látható eredmény a fenti kódból felépített API-tól származik — kipróbáltuk, nem kitaláltuk.

A kérés
Válassz egy kérést a gombokkal.
A válasz—
—
—

A bal oldali gombokkal küldhetsz kérést. A jobb oldalon a státuszkód, a fontos fejlécek és a válasz törzse jelenik meg.

Eredmények — sikeres esetek

KérésStátuszVálasz
GET /api/products2003 elem, X-Total-Count: 3
GET /api/products?categoryId=32002 elem, osszesTalalat: 2
GET /api/products?q=egÉR2001 elem — a keresés nem érzékeny a kis- és nagybetűre
GET /api/products?page=2&limit=2200a 3. elem, osszesTalalat: 3
GET /api/products/1200a termék
POST /api/products201Location: /api/products/4
DELETE /api/products/2204(üres törzs)
OPTIONS /api/products/1200Allow: DELETE, GET, HEAD, PATCH, PUT — cors() nélkül

Eredmények — hibás esetek

KérésStátuszcode
GET /api/products/alma400ERVENYTELEN_ID
GET /api/products/-3400ERVENYTELEN_ID
GET /api/products/999404TERMEK_NEM_TALALHATO
POST a { "ar": 100 } törzzsel400HIANYZO_MEZO
POST a { "nev": "X", "ar": "sok" } törzzsel400ERVENYTELEN_AR
POST a { "nev": "X", "ar": -5 } törzzsel400ERVENYTELEN_AR
POST a { …, "keszlet": 1.5 } törzzsel400ERVENYTELEN_KESZLET
POST a {"nev": hibas} törzzsel400ERVENYTELEN_JSON
PATCH üres ({}) törzzsel400NINCS_MODOSITAS
PUT /api/products/999404TERMEK_NEM_TALALHATO
GET /nincs-ilyen404NINCS_VEGPONT
A POST ÜRES TÖRZSEL

Ha csak a Content-Type: application/json fejlécet küldöd, törzs nélkül, a req.body értéke {} lesz (5. fejezet), és a validáció HIANYZO_MEZO hibát ad. Ez helyes: a kérés tényleg hiányos volt.

10Amit így sem tudunk

Ez a lista nem hiányérzet, hanem a következő blokkok tartalomjegyzéke:

HiányzikMiért baj?Mikor jön?
Tartós tárolásÚjraindításkor minden adat elveszik3. blokk — Prisma + MySQL
KapcsolatokA categoryId csak egy szám; nincs mögötte valódi kategória3. blokk
Validációs könyvtárA kézi validáció hosszú és könnyen hiányos3. blokk — Zod
HitelesítésBárki törölhet bármit4. blokk — JWT
JogosultságNincs különbség admin és vásárló között4. blokk
TesztekMinden ellenőrzés kézi4. blokk — node --test
Több egyidejű írásKét párhuzamos POST ugyanazt az id-t kaphatná3. blokk — az adatbázis megoldja
AMI VISZONT MEGMARAD

A végponttérkép, a státuszkódok, a hibaformátum, a rétegek és a validáció helye mind változatlan lesz. A 3. blokkban egyetlen fájl, a termek.service.js belseje cserélődik ki — és pontosan ezért érte meg rétegekre bontani.

11Gyakori hibák

Az id-t a kliens küldi

const termek = { ...req.body };            // ❌ a kliens id-je is bekerül

Tünet: a kliens felülírhat egy létező terméket, vagy ütköző azonosítót hoz létre.
Megoldás: a validáló függvény csak az ismert mezőket adja vissza, az id-t a service adja.

A PATCH letörli a mezőket

export function modosit(req, res) {
  const adat = termekAdatok(req.body);      // ❌ hiányzik a { reszleges: true }
}

Tünet: a PATCH ugyanúgy megköveteli a nev és ar mezőt, mint a PUT.
Megoldás: a reszleges kapcsoló átadása.

A lapozás rossz számot ad vissza

const elemek = talalatok.slice(kezdet, kezdet + meret);
const osszesTalalat = elemek.length;        // ❌ az OLDAL mérete, nem az összes találat

Tünet: a kliens azt hiszi, csak egy oldalnyi találat van.
Megoldás: az osszesTalalat értékét a lapozás előtt, a szűrt listából vedd.

DELETE után is 200-at küldesz törzzsel

Tünet: a kliens megpróbálja értelmezni a választ, pedig nincs mit.
Megoldás: res.sendStatus(204).

OPTIONS kérésre 404

Ok: router.route("/:id").all(…) használata.
Megoldás: a middleware-t metódusonként add meg.

Ha 404 helyett 204-et kapsz Allow fejléc nélkül, az nem ez a hiba: akkor a cors() middleware válaszolt az előkérésre. Az a helyes viselkedés.

A szűrés nem talál semmit

talalatok.filter(t => t.categoryId === req.query.categoryId);   // ❌ "3" === 3 hamis

Megoldás: Number(req.query.categoryId) — a 4. fejezet buktatója.

12Feladatok

Elméleti

F1⭐PUT vagy PATCH?

Döntsd el mindegyikről, és indokold:

  • a) a felhasználó átírja a termék árát az űrlapon
  • b) az adminisztrátor teljesen újra kitölti a termék adatlapját
  • c) a raktáros beállítja a készletet 12-re
  • d) egy import-szkript feltölti a teljes terméklistát külső forrásból
F2⭐Milyen státuszkód?

Melyik kód jár, és miért?

  • a) sikeres törlés · b) sikeres létrehozás · c) nem létező azonosító
  • d) hiányzó kötelező mező · e) nem szám az azonosító · f) sikeres lekérés
F3⭐⭐Hibakeresés

Mi a baj, és mi a tünete?

export function listaz(req, res) {
  const oldal = Number(req.query.page) || 1;
  const meret = Number(req.query.limit) || 20;
  const kezdet = (oldal - 1) * meret;
  const elemek = termekek.slice(kezdet, kezdet + meret);
  res.json({ elemek, osszesTalalat: elemek.length });
}
F4⭐⭐Miért veszélyes?

Miért nem adjuk tovább a nyers req.body-t a service-nek? Mondj két konkrét támadási lehetőséget, amit a mezőszűrés megakadályoz!

F5⭐⭐⭐Tervezz erőforrást

Írd le a rendelések teljes szerződését: végponttérkép, a rendelés alakja, a kötelező és elhagyható mezők, a query paraméterek, a hibakódok. Gondolj arra is, hogy egy rendelés tételeket tartalmaz, és hogy egy leadott rendelést nem lehet akárhogy módosítani!

Gyakorlati

G1⭐Olvasás

Építsd fel a projektet a 2. szakasz szerint, és készítsd el a két olvasó végpontot (GET /api/products és GET /api/products/:id) a rétegekkel együtt. Próbáld ki REST Clienttel!

G2⭐⭐Létrehozás validációval

Készítsd el a POST végpontot és a termekAdatok függvényt. Bizonyítsd be mind az öt hibaesetet (hiányzó név, hiányzó ár, nem szám ár, negatív ár, tört készlet), és jegyezd fel a státuszkódot és a hibakódot!

G3⭐⭐Módosítás és törlés

Egészítsd ki a PUT, PATCH és DELETE végpontokkal. Bizonyítsd be a PUT és a PATCH különbségét: a 7. szakasz példájával mutasd meg, hogy a PUT alapértékre állítja a nem küldött keszlet mezőt, a PATCH viszont nem!

G4⭐⭐Szűrés és lapozás

Egészítsd ki a listavégpontot a categoryId, q, page és limit paraméterekkel. Ellenőrizd, hogy az osszesTalalat a szűrt találatok számát adja, nem az oldalét!

G5⭐⭐⭐Második erőforrás

Készítsd el ugyanígy a kategóriákat (/api/categories), teljes CRUD-dal. Egészítsd ki egy GET /api/categories/:id/products végponttal, amely az adott kategória termékeit adja vissza (4. fejezet: mergeParams). Gondoskodj róla, hogy nem létező kategóriánál 404 legyen a válasz!

13Önellenőrző kvíz

0 / 10 megválaszolva

14Összegzés

  1. A CRUD négy művelete hat végpont: két olvasó, egy létrehozó, kettő módosító és egy törlő. Mindegyikhez tartozik egy helyes státuszkód.
  2. A munka a szerződéssel kezdődik: végponttérkép, az erőforrás alakja, a query paraméterek és a hibakódok — kód előtt.
  3. A service memóriában dolgozik, null-t vagy false-t ad vissza, ha nincs találat, és nem ismeri a HTTP-t.
  4. A validáció egy helyen van, a reszleges kapcsolóval szolgálja ki a POST, PUT és PATCH végpontot is, és csak az ismert mezőket engedi tovább.
  5. A PUT teljes cserét jelent (a hiányzó mezők alapértékre állnak), a PATCH részleges módosítást (a nem küldött mezők változatlanok). Mérve is megmutattuk.
  6. A lapozásnál az osszesTalalat a szűrt találatok száma, a lapozás előtt számolva.
  7. Az azonosító ellenőrzése middleware, mert négy végpontnak is ugyanaz a dolga.
  8. A router.route("/:id").all(…) rövidebbnek tűnik, de elrontja az OPTIONS kezelését — a middleware-t metódusonként adjuk meg.
  9. Ami hiányzik (tartós tárolás, hitelesítés, tesztek), az a következő két blokk témája — és a rétegezés miatt ezekhez egyetlen fájlt kell majd kicserélni.

15Fogalomtár

FogalomJelentés
CRUDCreate · Read · Update · Delete — a négy alapművelet
VégponttérképAz API összes végpontja metódussal, útvonallal, státuszkódokkal
SzerződésAmit az API ígér: mit fogad el és mit ad vissza
PUTTeljes felülírás; a hiányzó mezők alapértékre állnak
PATCHRészleges módosítás; a nem küldött mezők változatlanok
ValidációA bejövő adat ellenőrzése és tisztítása
MezőszűrésCsak az ismert mezők átvétele — védekezés az id felülírása ellen
Lapozáspage és limit alapján az eredmény egy szeletének visszaadása
X-Total-CountFejléc az összes találat számával
204 No ContentSikeres művelet, nincs visszaadandó tartalom
← Előző 8. Hibakezelés