Express alapok · 7. tananyag
Két dolog kerül egy fejezetbe, mert mindkettő ugyanarról szól: hol legyen a kód és a tartalom.
A második fele a fontosabb — eddig minden az app.js-ben
volt, és ez már kezd fájni.
express.static()
middleware-rel;import.meta.dirname
használatával;import express from "express"; import path from "node:path"; const app = express(); app.use(express.static(path.join(import.meta.dirname, "public")));
Ettől a public mappa tartalma közvetlenül elérhető lesz.
Az eredmények:
| Kérés | Válasz | Content-Type |
|---|---|---|
GET / | a public/index.html tartalma | text/html; charset=utf-8 |
GET /index.html | ugyanaz | text/html; charset=utf-8 |
GET /stilus.css | a public/stilus.css | text/css; charset=utf-8 |
GET /kepek/logo.svg | a public/kepek/logo.svg | image/svg+xml |
GET /.titok | 404 — a ponttal kezdődő fájlokat nem adja ki | — |
GET /nincsilyen.css | 404 — továbbmegy a láncban | — |
Ez a három dolog kézzel is megírható lenne (a 2. fejezetben pont ezt soroltuk fel a hiányzó funkciók közt), de nem kell:
| Amit hozzátesz | Érték | Mire jó? |
|---|---|---|
| MIME-típus | text/css, image/svg+xml, text/html… | A böngésző tudja, mit kezdjen a fájllal |
Last-Modified | a fájl módosítási ideje | A böngésző rákérdezhet: „változott azóta?” |
Cache-Control | public, max-age=0 | Gyorsítótárazható, de minden használat előtt ellenőrizni kell |
max-age=0?
Azt, hogy a böngésző eltárolhatja a fájlt, de mielőtt újra használná, meg kell kérdeznie
a szervert, változott-e. A szerver a Last-Modified alapján válaszolhat
304 Not Modified kóddal — ilyenkor a fájl nem megy át újra a
hálózaton. Fejlesztéskor ez tökéletes: mindig a friss fájlt látod, de nem terheled feleslegesen
a hálózatot.
Ez a fejezet első felének legfontosabb buktatója. A rövidebb írásmód működni látszik:
app.use(express.static("public")); // ⚠️ csak néha jó
A gond: az express.static("public") az útvonalat nem a fájlhoz
képest értelmezi, hanem ahhoz a mappához, ahonnan a szervert elindítottad
(process.cwd()).
A megoldás mindig ugyanaz: abszolút útvonal.
import path from "node:path"; app.use(express.static(path.join(import.meta.dirname, "public"))); // ✅ mindig jó
| Elem | Mit ad? |
|---|---|
import.meta | Annak a fájlnak a mappája, amelyikben a sor áll — sosem változik |
path.join(a, b) | Összefűzi az útvonalrészeket a rendszernek megfelelő elválasztóval (Windows: \, Linux/macOS: /) |
import.meta.dirname ÚJ ÉS KÉNYELMES
Régebbi példákban ezt a két sort látod helyette:
import { fileURLToPath } from "node:url" …
const __dirname = path.dirname(fileURLToPath(import.meta.url)). Ugyanaz, csak
sokkal körülményesebb. A Node 24-ben már nem kell.
Mert a "public/kepek" írásmód Windowson is működik ugyan, de fordítva
("public\\kepek") Linuxon már nem. A
path.join("public", "kepek") mindenhol helyes eredményt ad — és a projektjeink
éles környezetben Linuxon futnak majd.
| Helyzet | Kód | Eredmény |
|---|---|---|
| Előtag alá helyezés | app.use("/eszkozok", express.static(…)) | A public/stilus.css a /eszkozok/stilus.css címen |
| Két statikus mappa | két app.use(express.static(…)) egymás után | Az első találat nyer — ugyanaz a szabály, mint az útvonalaknál |
Mappa index.html nélkül | GET /kepek | 301 átirányítás a /kepek/ alakra, majd 404 |
app.use(helmet()); app.use(express.json()); app.use(express.static(path.join(import.meta.dirname, "public"))); // ← ide app.use("/api/products", termekRouter); app.use(nemTalalhato);
A statikus kiszolgálás az API-útvonalak előtt áll. Így a fájlkérések azonnal kiszolgálódnak, és nem futnak végig feleslegesen az útvonal-kereséssel. Ha nincs egyező fájl, a middleware csendben továbbadja a kérést — ezért nem zavarja az API-t.
index, maxAge,
dotfiles, extensions,
redirect — ott van mindegyikhez a lehetséges értékek és a magyarázat. Az
alapértékek jók.
res.sendFile()app.get("/dokumentacio", (req, res) => { res.sendFile(path.join(import.meta.dirname, "docs", "api.html")); });
res.sendFile() ABSZOLÚT ÚTVONALAT VÁR
Relatívval hibát dob, hacsak nem adsz meg { root: … } beállítást. Itt is az
import.meta a megoldás.
Adott egy express.static() hívás és egy fájl a mappában.
Válaszd ki, milyen címen éri el a kliens!
Nézzük meg, mi minden van egy „ömlesztett” végpontban:
app.get("/api/products/:id", (req, res) => { const id = Number(req.params.id); // 1) bemenet ellenőrzése if (Number.isNaN(id)) return res.status(400).json({ /* … */ }); // 2) HTTP-válasz const termek = termekek.find(t => t.id === id); // 3) adathozzáférés if (!termek) return res.status(404).json({ /* … */ }); // 4) HTTP-válasz res.json(termek); // 5) HTTP-válasz });
Öt sorban három különböző felelősség keveredik. Ez rövid példán nem gond, de:
| Probléma | Mikor fáj? |
|---|---|
| Nem tesztelhető külön | A terméklogikát csak HTTP-kérésen keresztül tudod kipróbálni |
| Nem újrafelhasználható | Ha egy másik végpontnak is kell a keresés, másolnod kell |
| Nehezen cserélhető | Ha tömb helyett adatbázis lesz, minden végpontot át kell írni |
| Nehezen olvasható | Ötszáz sor után nem találod, hol van a „mi az akciós ár” szabály |
A 3. blokkban ugyanezt az API-t Prismára ültetjük át. Ha az adathozzáférés külön rétegben van, csak azt az egy fájlt kell átírni — a végpontok, a státuszkódok, a hibaformátum mind marad.
req és res.| Réteg | Fájl | Mit csinál? | Mit NEM csinál? |
|---|---|---|---|
| Route | routes/termek | Összeköti az útvonalmintát a vezérlővel | Nincs benne logika, se válaszküldés |
| Controller (vezérlő) | controllers/termek | Beolvassa a bemenetet, hívja a service-t, HTTP-választ küld | Nem tud az adattárolásról |
| Service (szolgáltatás) | services/termek | Az üzleti logika és az adatkezelés | Nem ismeri a req-et és a res-t! |
app.get("/api/products/:id", (req, res) => { const id = Number(req.params.id); if (Number.isNaN(id)) return res.status(400).json({…}); const termek = termekek .find(t => t.id === id); if (!termek) return res.status(404).json({…}); res.json(termek); });
// routes/termek.routes.js router.get("/:id", vezerlo.egy); // controllers/termek.controller.js export function egy(req, res) { const id = Number(req.params.id); // …ellenőrzés + válasz… } // services/termek.service.js export function termekAzonositoval(id) { return termekek.find(t => t.id === id) ?? null; }
// src/routes/termek.routes.js import express from "express"; import * as vezerlo from "../controllers/termek.controller.js"; const router = express.Router(); router.get("/", vezerlo.listaz); router.get("/:id", vezerlo.egy); router.post("/", vezerlo.letrehoz); export default router;
Ez a fájl egy pillantással áttekinthető: azonnal látod, milyen végpontjai vannak az erőforrásnak. Ez önmagában is nagy nyereség.
// src/controllers/termek.controller.js import * as szolgaltatas from "../services/termek.service.js"; export function listaz(req, res) { const categoryId = req.query.categoryId ? Number(req.query.categoryId) : undefined; res.json(szolgaltatas.osszesTermek({ categoryId })); } export function egy(req, res) { const id = Number(req.params.id); if (Number.isNaN(id)) { return res.status(400).json({ error: { message: "Az azonosító csak szám lehet.", code: "ERVENYTELEN_ID" } }); } const termek = szolgaltatas.termekAzonositoval(id); if (!termek) { return res.status(404).json({ error: { message: "Nincs ilyen termék.", code: "TERMEK_NEM_TALALHATO" } }); } res.json(termek); } export function letrehoz(req, res) { const termek = szolgaltatas.ujTermek(req.body); res.location(`/api/products/${termek.id}`).status(201).json(termek); }
A vezérlő feladata a fordítás: a HTTP-kérésből kiveszi az adatot, átadja a service-nek, majd az eredményt HTTP-válasszá alakítja — a helyes státuszkóddal.
res.location(…).status(201).json(…) LÁNCOLHATÓ
Mindhárom metódus a res objektumot adja vissza, ezért egy sorba fűzhetők.
Kipróbálva: a válasz 201-es kóddal és
Location: /api/products/44 fejléccel érkezik.
// src/services/termek.service.js const termekek = [ { id: 42, nev: "Billentyűzet", ar: 12990, categoryId: 3 }, { id: 43, nev: "Egér", ar: 4990, categoryId: 3 } ]; let kovetkezoId = 44; export function osszesTermek({ categoryId } = {}) { if (categoryId === undefined) return termekek; return termekek.filter(t => t.categoryId === categoryId); } export function termekAzonositoval(id) { return termekek.find(t => t.id === id) ?? null; } export function ujTermek(adat) { const termek = { ...adat, id: kovetkezoId++ }; // az id a spread UTÁN áll — lásd lentebb! termekek.push(termek); return termek; }
Figyeld meg, mi nincs benne: se req, se
res, se státuszkód. Ha nincs találat, null-t ad
vissza — hogy abból 404 lesz-e vagy valami más,
a vezérlő dolga.
Az objektumban a később álló mező nyer. Ha
{ id: kovetkezoId++, ...adat } alakot írnál, és a kliens
{"nev":"X","ar":1,"id":9999} törzset küldene, az
...adat felülírná a szerver által adott azonosítót — a
termék id-ja 9999 lenne. Ezért áll az id a
spread után.
Ez ugyanaz az elv, mint az 1. blokk 1. fejezetében: a klienstől érkező adatot nem hisszük
el. A 9. fejezetben ennél is szigorúbb megoldást használunk: ott mezőnként felsoroljuk, mit
veszünk át — így az ismeretlen mezők (admin,
szerep) be sem kerülnek az adatba.
?? null ÉS NEM CSAK A find EREDMÉNYE?
A find nem találat esetén undefined-ot ad. A
null kifejezett jelzés: „megkerestem, nincs ilyen”. Az
undefined inkább azt sugallja: „nem is tudom, mi ez”. Kis különbség, de
olvashatóbbá teszi a vezérlőt.
Nyolc kódrészlet a webshop API-ból. Döntsd el mindegyikről, melyik rétegben a helye — ez az a döntés, amit a 9. fejezetben végig alkalmazni fogunk.
webshop-api/ package.json .env .gitignore index.js ← a szerver indítása public/ ← statikus fájlok index.html stilus.css src/ app.js ← az alkalmazás felépítése config.js ← beállítások egy helyen routes/ termek.routes.js kategoria.routes.js controllers/ termek.controller.js kategoria.controller.js services/ termek.service.js kategoria.service.js middlewares/ naplozo.js nem-talalhato.js hibakezelo.js utils/ valaszok.js ← a hiba() segédfüggvény az 5. fejezetből
app.js// src/app.js import express from "express"; import path from "node:path"; import termekRouter from "./routes/termek.routes.js"; const app = express(); app.use(express.json()); app.use(express.static(path.join(import.meta.dirname, "..", "public"))); app.use("/api/products", termekRouter); app.use((req, res) => { res.status(404).json({ error: { message: "Nincs ilyen végpont.", code: "NINCS_VEGPONT" } }); }); export default app;
A ".." azért kell, mert az app.js az
src/ mappában van, a public/ pedig eggyel feljebb.
A fenti fájlokból felépített API mind a hat vizsgált kérésre helyesen válaszolt:
200 a listára és az egy termékre, 400 a hibás
azonosítóra, 404 a nem létezőre, 201 +
Location fejléc a létrehozásra, és a statikus
stilus.css is kiszolgálódott.
Kövessük a GET /api/products/42 kérést:
| # | Hol? | Mi történik? |
|---|---|---|
| 1 | index.js | (már lefutott indításkor) elindította a szervert |
| 2 | app.js | express.json() — nincs törzs, továbbmegy |
| 3 | app.js | express.static() — nincs ilyen fájl, továbbmegy |
| 4 | app.js | app.use("/api/products", termekRouter) — illeszkedik, belép a routerbe |
| 5 | termek.routes.js | router.get("/:id") illeszkedik → vezerlo.egy |
| 6 | termek.controller.js | Number("42") → 42, nem NaN, mehet tovább |
| 7 | termek.service.js | termekAzonositoval(42) → megtalálja az objektumot |
| 8 | termek.controller.js | res.json(termek) → 200 |
Az eredmény:
GET /api/products/42 → 200 {"id":42,"nev":"Billentyűzet","ar":12990,"categoryId":3}
GET /api/products/alma → 400 {"error":{"message":"Az azonosító csak szám lehet.", …}}
GET /api/products/999 → 404 {"error":{"message":"Nincs ilyen termék.", …}}
Nincs kőbe vésett szabály, de legyél következetes. Amit mi használunk:
| Elem | Írásmód | Példa |
|---|---|---|
| Fájlnév | kisbetű, kötőjellel, a szerep a végén | termek.routes.js, nem-talalhato.js |
| Mappanév | kisbetű, többes szám | routes/, controllers/, services/ |
| Függvénynév | magyar, beszédes ige | listaz, termekAzonositoval |
| Router-változó | mindig router a saját fájljában | const router = express.Router() |
termek.routes.js ÉS NEM termekRoutes.js?
Mert a fájlnevek kis- és nagybetű kezelése rendszerfüggő: Windowson mindegy, Linuxon nem. A csupa kisbetűs, kötőjeles vagy pontos írásmóddal soha nem futsz bele abba, hogy a saját gépeden működik, a szerveren nem.
import * as vezerlo from "../controllers/termek.controller.js"; // ✅ így írjuk router.get("/:id", vezerlo.egy); import { egy, listaz } from "../controllers/termek.controller.js"; // ugyanaz, másképp router.get("/:id", egy);
Mindkettő helyes. Mi a csillagosat használjuk, mert a hívás helyén látszik,
honnan jön a függvény (vezerlo.egy,
szolgaltatas.osszesTermek) — ez hosszabb fájlokban sokat segít.
A 3. fejezetben a portot már környezeti változóból vettük. Ha több ilyen beállítás lesz, érdemes egy helyre gyűjteni:
// src/config.js export const config = { port: Number(process.env.PORT) || 3000, kornyezet: process.env.NODE_ENV || "development", oldalMeret: Number(process.env.OLDAL_MERET) || 20, torzsMeret: process.env.TORZS_MERET || "100kb" };
// index.js import app from "./src/app.js"; import { config } from "./src/config.js"; app.listen(config.port, () => { console.log(`A szerver fut (${config.kornyezet}): http://localhost:${config.port}`); });
| Előny | Magyarázat |
|---|---|
| Egy helyen látod | Nem kell process.env-et vadászni tíz fájlban |
| Az átalakítás egyszer történik | A Number(…) konverzió és az alapérték itt van, nem szétszórva |
| Könnyen dokumentálható | A .env.example fájl ugyanezeket a kulcsokat sorolja fel |
.env FÁJL SOHA NEM MEGY VERZIÓKEZELŐBE
(1. blokk, 3. fejezet.) Helyette egy .env.example fájlt teszünk mellé, üres
vagy példaértékekkel — abból tudja a munkatársad, mit kell beállítania.
app.use(express.static("public")); // ❌ a CWD-hez képest keresi
Tünet: a saját gépeden működik, a szerveren vagy más indítási módnál nem.
Megoldás: path.join(import.meta.
res.sendFile() hibát dobTünet:
TypeError: path must be absolute or specify root to res.sendFile.
Megoldás:
res.sendFile(path.join(import.meta.
Ha véletlenül van egy public/api mappa, akkor a
/api/… kérések oda futnak be. Ritka, de alattomos.
Megoldás: a statikus tartalmat tedd előtag alá
(app.use("/eszkozok", …)), vagy ügyelj rá, hogy a
public mappában ne legyen az API-útvonalakkal ütköző név.
// termek.service.js → importálja a controllert // termek.controller.js → importálja a service-t
Tünet: valamelyik import undefined lesz, és hívásnál
TypeError: … is not a function.
Megoldás: a rétegek csak lefelé hivatkoznak: route → controller →
service. Visszafelé soha. Ha mégis kellene, az azt jelzi, hogy rossz rétegbe került egy függvény.
Cannot find module './routes/termek.routes'Tünet: hiányzik a .js kiterjesztés az importból.
Megoldás: ESM-ben kötelező kiírni:
from "./routes/termek.routes.js".
app.use("/api/products", termekRouter); router.get("/api/products/:id", …); // ❌
A 4. fejezetből ismerős: a routerben relatív útvonal áll
("/:id").
Elméleti
Sorold be mindegyiket route, controller vagy service rétegbe:
res.status(404).json(…) router.get("/:id", …)Number(req.params.id) átalakításLocation fejléc beállításaEgy munkatársad ezt írta, és nála működik, a szerveren nem. Miért?
app.use(express.static("public"));
Miért hibás ez a függvény, és hogyan javítanád?
// termek.service.js export function termekAzonositoval(req, res) { const termek = termekek.find(t => t.id === Number(req.params.id)); if (!termek) return res.status(404).json({ error: { message: "Nincs ilyen." } }); return termek; }
Írd le lépésről lépésre, mi történik egy POST /api/products kéréssel a
4. szakasz szerkezetében, a szerver belépésétől a válaszig. Nevezd meg minden lépésnél a
fájlt is!
A webshopba jön egy új szabály: 20 000 Ft fölött ingyenes a szállítás. Melyik rétegbe kerül ez a szabály, és miért? Mi történne, ha a vezérlőbe írnád? Mi történne, ha a kliensoldali kódba?
Gyakorlati
Készíts egy public/index.html fájlt, amely felsorolja az API végpontjait,
és szolgáld ki az express.static() middleware-rel. Ellenőrizd, hogy a
/ címen megjelenik!
Írd meg mindkét változatot ("public" és
path.join(import.meta) külön előtag alá, majd
indítsd a szervert egyszer a projekt mappájából, egyszer eggyel feljebbről. Készíts táblázatot a négy
eredményről!
Vedd az 5. fejezet POST /api/products végpontját, és bontsd szét route,
controller és service fájlokra a 4. szakasz szerkezete szerint. A service-ben ne legyen se
req, se res!
Készítsd el ugyanígy a kategóriákat: kategoria.routes.js,
kategoria.controller.js, kategoria.service.js,
végpontok: GET /api/categories,
GET /api/categories/:id.
Emeld ki a portot, az alapértelmezett oldalméretet és a törzs méretkorlátját egy
config.js fájlba, és használd őket a megfelelő helyeken. Készíts hozzá
.env.example fájlt, és bizonyítsd be, hogy a .env
átírása tényleg hat a működésre!
express.static() egyetlen sorral kiszolgál egy egész
mappát, és magától beállítja a MIME-típust, a
Last-Modified és a
Cache-Control fejlécet..env) alapból nem adja ki.path.join(import.meta.dirname, "public") . A relatív írásmód az
indítási mappához képest keres, és más indítási módnál 404-et ad.req,
res és státuszkód. Ezért tesztelhető külön, és ezért cserélhető alatta az
adatforrás.config.js), a titkok
pedig a .env-be, ami soha nem megy verziókezelőbe.| Fogalom | Jelentés |
|---|---|
| Statikus fájl | Változatlan tartalom, amit a szerver egy az egyben visszaad (kép, CSS, HTML) |
express.static() | Beépített middleware statikus fájlok kiszolgálására |
import.meta | Annak a fájlnak a mappája, amelyikben a sor áll |
path.join() | Útvonalrészek összefűzése a rendszernek megfelelő elválasztóval |
process.cwd() | Az a mappa, ahonnan a programot elindították |
res.sendFile() | Egyetlen fájl kiszolgálása; abszolút útvonalat vár |
| Réteg (layer) | A kód egy jól elhatárolt felelősségi köre |
| Route-réteg | Az útvonalmintát köti a vezérlőhöz |
| Controller (vezérlő) | A HTTP-kérésből adatot vesz, és HTTP-választ küld |
| Service (szolgáltatás) | Az üzleti logika és az adatkezelés — HTTP nélkül |
| Körkörös import | Két modul kölcsönösen importálja egymást — kerülendő |
.env.example | A .env fájl mintája, ami mehet verziókezelőbe |