Express alapok · 2. tananyag
A következő fejezettől Expresst fogunk használni — jogos a kérdés, miért írunk most szervert kézzel. Két okból: hogy lásd, mi történik valójában, és hogy megértsd, mit ad hozzá a keretrendszer. Ez a fejezet adja meg az értelmét mindannak, ami utána jön.
http moduljával;Content-Type fejléccel;URL osztállyal;EADDRINUSE hibát és a „nem válaszol a szerver” tünetet.Hozz létre egy új projektet (az 1. blokk 3. fejezetéből ismert módon), és benne egy
szerver.js fájlt:
import http from "node:http"; const server = http.createServer((keres, valasz) => { valasz.end("Szia a szerverről!"); }); server.listen(3000, () => { console.log("A szerver fut: http://localhost:3000"); });
Futtatás:
node --watch szerver.js
Amit látnod kell:
A szerver fut: http://localhost:3000
Nyisd meg a böngészőben a http://localhost:3000 címet — megjelenik a szöveg.
node: ELŐTAG
Az import http from "node:http" írásmód egyértelműen jelzi, hogy a Node
beépített moduljáról van szó, nem npm-csomagról. Működne nélküle is
("http"), de a node: előtag biztonságosabb:
nem lehet összetéveszteni egy hasonló nevű, letöltött csomaggal.
| Elem | Mit csinál? |
|---|---|
http.createServer(fv) | Létrehoz egy szerverobjektumot. A megadott függvényt minden beérkező kérésnél meghívja |
keres (szokásos neve: req) | A beérkező kérés — ebből tudjuk meg, mit akar a kliens |
valasz (szokásos neve: res) | A válasz, amit mi töltünk meg tartalommal |
valasz.end(szoveg) | Elküldi a választ és lezárja a kapcsolatot |
server.listen(3000, fv) | Elindítja a szervert a 3000-es porton. A függvény egyszer fut le, indításkor |
req ÉS res LESZ
Innentől ezekkel a rövid nevekkel hivatkozunk rájuk, mert minden dokumentációban és példakódban így szerepelnek. Itt csak azért írtuk ki magyarul, hogy egyértelmű legyen a szerepük.
Ez a fejezet legfontosabb gondolata — sokan itt csúsznak el.
console.log("1. Ez a program indulásakor fut le, EGYSZER."); const server = http.createServer((req, res) => { console.log("3. Ez MINDEN KÉRÉSNÉL lefut újra."); res.end("ok"); }); server.listen(3000, () => { console.log("2. Ez is egyszer, amikor a szerver elindult."); });
Indítás után a terminálban:
1. Ez a program indulásakor fut le, EGYSZER.
2. Ez is egyszer, amikor a szerver elindult.
…és a program nem lép ki. Vár. Amikor megnyitod a böngészőben:
3. Ez MINDEN KÉRÉSNÉL lefut újra.
3. Ez MINDEN KÉRÉSNÉL lefut újra.
Mert a böngésző a lap mellett elkéri a /favicon.ico fájlt is — az is egy
kérés. (Ellenőrizve: egyetlen lapmegnyitás két callback-futást eredményez.) Ha ez zavar, a
hálózati fülön (F12 → Network) pontosan látod, mi mit kért le.
A szerver nem „fut le”, hanem fut. Egy hagyományos program elindul, dolgozik, kilép.
A szerver elindul, és vár — a listen() hívás után a Node
életben tartja a folyamatot, mert tudja, hogy kérésekre kell figyelnie. Leállítás:
Ctrl + C.
Hat kódrészlet a szerverfájlból. Döntsd el mindegyikről, hogy egyszer fut-e le indításkor, vagy újra és újra, minden beérkező kérésnél.
A req objektumból három dolog érdekel minket most:
const server = http.createServer((req, res) => { console.log("Metódus:", req.method); // GET, POST, PUT… console.log("Útvonal:", req.url); // /api/products?categoryId=3 console.log("Fejlécek:", req.headers); // objektum: { host: …, accept: … } res.end("ok"); });
Ha a http://localhost:3000/api/products?categoryId=3&page=2 címet hívod
meg, a terminálban ez jelenik meg:
Metódus: GET
Útvonal: /api/products?categoryId=3&page=2
| Tulajdonság | Mit tartalmaz? | Buktató |
|---|---|---|
req.method |
A HTTP-metódus nagybetűvel: GET, POST… |
Mindig nagybetűs — a "get" összehasonlítás hamis lesz |
req.url |
Az útvonal és a query string | Nem tartalmazza a protokollt és a hostot! Csak /api/products?… |
req.headers |
A fejlécek objektumként | A kulcsok mind kisbetűsek, akárhogy küldte a kliens |
Az URL részei közül a szerverhez csak az útvonal és a query jut el —
a séma, a host és a port a kapcsolat felépítéséhez kellett, a töredék
(#valami) pedig el sem hagyja a böngészőt.
Egy HTTP-válasz három részből áll (1. blokk, 2. fejezet): státuszkód, fejlécek, törzs. Mindhármat mi állítjuk be:
const server = http.createServer((req, res) => { res.statusCode = 200; // 1) státuszkód res.setHeader("Content-Type", "application/json; charset=utf-8"); // 2) fejléc res.end(JSON.stringify({ uzenet: "Szia!" })); // 3) törzs + lezárás });
| Sor | Mit csinál? | Mi történne nélküle? |
|---|---|---|
res.statusCode = 200 | Beállítja a státuszkódot | Alapból 200 lenne — de hibánál nekünk kell 404-et vagy 500-at adnunk |
res.setHeader("Content-Type", …) | Megmondja, milyen formátumú a válasz | A böngésző találgatna; a JSON szövegként jelenne meg, az ékezetek elromolhatnának |
res.end(…) | Elküldi a törzset és lezárja a választ | A kérés soha nem fejeződne be — a böngésző csak pörögne |
Ugyanez egy sorral kevesebből:
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify({ uzenet: "Szia!" }));
A writeHead egyszerre állítja a státuszkódot és a fejléceket.
Utána viszont már nem változtathatod meg őket — erről a következő fejezetekben lesz
még szó.
A http modul nem ismeri az útvonalakat. Nekünk kell
szétválogatnunk a kéréseket:
import http from "node:http"; const server = http.createServer((req, res) => { res.setHeader("Content-Type", "application/json; charset=utf-8"); if (req.method === "GET" && req.url === "/api/products") { res.statusCode = 200; res.end(JSON.stringify([{ id: 42, nev: "Billentyűzet" }])); return; // ← FONTOS: itt kilépünk! } if (req.method === "GET" && req.url === "/api/categories") { res.statusCode = 200; res.end(JSON.stringify([{ id: 3, nev: "Perifériák" }])); return; } // ha idáig eljutottunk, egyik feltétel sem illeszkedett res.statusCode = 404; res.end(JSON.stringify({ error: { message: "Nincs ilyen végpont." } })); }); server.listen(3000, () => console.log("Fut: http://localhost:3000"));
return?
Mert a res.end() nem állítja meg a függvényt! Ha kihagynád,
a kód továbbfutna a következő if-re, majd a 404-es ágra — vagyis
kétszer próbálnánk válaszolni ugyanarra a kérésre.
Ilyenkor ezt kapod, és a szerver folyamata le is áll:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (node:_http_outgoing:684:11)
…
code: 'ERR_HTTP_HEADERS_SENT'
Magyarul: „a fejléceket már elküldtük a kliensnek, nem lehet újat beállítani”. Egy kérésre
egy válasz jut — ez a HTTP alapszabálya. A hibával és a mögötte álló szabállyal
az 5. fejezetben foglalkozunk részletesen; itt elég annyit megjegyezned, hogy
a hiányzó return okozza.
A req.url a query stringet is tartalmazza, ezért a fenti szigorú
összehasonlítás (req.url === "/api/products") elbukik,
amint a kliens szűrni akar:
/api/products?categoryId=3 ≠ /api/products
A megoldás a beépített URL osztály:
const cim = new URL(req.url, `http://${req.headers.host}`); console.log(cim.pathname); // "/api/products" console.log(cim.searchParams.get("categoryId")); // "3" console.log(cim.searchParams.get("page")); // "2"
| Elem | Mit ad? |
|---|---|
new URL(cim, alap) |
Feldolgozott URL-objektum. A második paraméter azért kell, mert a
req.url nem teljes URL — hiányzik belőle a host |
.pathname |
Csak az útvonal, query nélkül: /api/products |
.searchParams |
Egy query paraméter értéke — mindig szövegként! |
A searchParams a "2"
szöveget adja vissza, nem a 2 számot. A
"2" === 2 összehasonlítás hamis. Ha számként kell,
alakítsd át: Number(cim.searchParams.
Mi van a GET /api/products/42 végponttal? Az azonosító
az útvonal része, tehát így kellene kezelni:
const reszek = cim.pathname.split("/"); // ["", "api", "products", "42"] if (req.method === "GET" && reszek[1] === "api" && reszek[2] === "products" && reszek[3]) { const id = Number(reszek[3]); // … }
Ezt minden végponthoz meg kellene írni, kézzel. Az Expressben ez így néz majd ki:
app.get("/api/products/:id", (req, res) => { const id = Number(req.params.id); // … });
POST és PUT kérésnél a kliens
adatot is küld. Ez nem áll rendelkezésre azonnal: a törzs
darabokban (chunk) érkezik a hálózaton, ezért meg kell várni a végét.
for await ciklus fűzi össze őket.A modern, olvasható megoldás — és itt jön jól az előző fejezet:
const server = http.createServer(async (req, res) => { if (req.method === "POST" && req.url === "/api/products") { let torzs = ""; for await (const darab of req) { // a req „aszinkron bejárható” torzs += darab; // darabonként összefűzzük } const adat = JSON.parse(torzs); // szövegből objektum res.statusCode = 201; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify({ letrehozva: adat })); return; } res.statusCode = 404; res.end(); });
Kipróbálás REST Clienttel (1. blokk, 5. fejezet — .http fájl):
POST http://localhost:3000/api/products Content-Type: application/json { "name": "Teszt", "price": 100 }
A válasz:
{ "letrehozva": { "name": "Teszt", "price": 100 } }
req.body?Mert nincs olyan. A http modul nem olvassa be helyetted
a törzset — te döntöd el, mikor és hogyan. Ez rugalmas (nagy fájlfeltöltésnél nem is akarnád a
memóriába tölteni), de a mindennapi JSON-nál csak macera.
Az express.json() middleware elvégzi ezt a beolvasást és a
JSON.parse-t is, az eredményt pedig a
req.body objektumba teszi. A 6. fejezetben látjuk majd, hogyan.
Régebbi példákban eseménykezelőkkel találkozol — ugyanaz, csak nehezebben olvasható:
let torzs = ""; req.on("data", (darab) => { torzs += darab; }); req.on("end", () => { const adat = JSON.parse(torzs); // … });
Írjuk össze, mi mindent kellene még megvalósítanunk egy valódi API-hoz:
| Feladat | Mennyi munka kézzel? |
|---|---|
Útvonalparaméterek (/products/:id) | Szétdarabolás és összehasonlítás minden végpontnál |
| A törzs beolvasása és JSON-ná alakítása | Minden POST/PUT végpontnál újra |
| Hibás JSON kezelése | try/catch a JSON.parse köré, mindenhol |
| Egységes 404-es válasz | Kézzel, a lánc végén |
| Központi hibakezelés | Nincs — minden try/catch a mi dolgunk |
| Statikus fájlok kiszolgálása | Fájl beolvasása + MIME-típus meghatározása kiterjesztés szerint |
| Közös lépések (naplózás, hitelesítés) | Minden végpont elején újra meghívva |
Válasz-segédfüggvények (res.json) | Nincsenek — kézzel setHeader + JSON |
| CORS, tömörítés, biztonsági fejlécek | Mind kézzel |
Ez a lista nem elrettentés: pontosan ezeket kapjuk meg az Expressben. Ezért éri meg
keretrendszert használni — nem azért, mert a http modul rossz, hanem mert
a fenti listát senki nem akarja minden projektben újraírni.
„Adj vissza egy JSON-listát a GET /api/products címen”:
http modul15 SORimport http from "node:http"; const server = http.createServer((req, res) => { const cim = new URL(req.url, `http://${req.headers.host}`); if (req.method === "GET" && cim.pathname === "/api/products") { res.statusCode = 200; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify(termekek)); return; } res.statusCode = 404; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify( { error: "Nincs ilyen végpont." })); }); server.listen(3000);
import express from "express"; const app = express(); app.get("/api/products", (req, res) => { res.json(termekek); }); app.listen(3000);
Ugyanaz a működés — a második változatban a státuszkód, a Content-Type és a
JSON mind automatikus. Ráadásul a 404-es ágat sem kell megírni:
az Express magától válaszol, ha egyik útvonal sem illeszkedik.
EADDRINUSE — a port foglaltEz a leggyakoribb hiba a szerverekkel való ismerkedés során. Ha egyszerre kétszer indítod el a szervert (vagy elfelejtetted leállítani az előzőt), ezt kapod:
node:events:505
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:2324:16)
at listenInCluster (node:net:2433:12)
at Server.listen (node:net:2558:7)
| Kérdés | Válasz |
|---|---|
| Mit jelent? | A 3000-es porton már figyel egy program |
| Miért történik? | Szinte mindig egy korábban elindított saját szervered fut még — például egy másik terminálablakban |
| Megoldás | Keresd meg a futó szervert, és állítsd le Ctrl+C-vel. Ha nem találod, indítsd másik porton, vagy zárd be az összes terminált |
Ezért érdemes a portot változóból venni:
const PORT = process — így egy
.env fájlból vagy parancssorból is átállítható, kódmódosítás nélkül.
const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader("Content-Type", "text/plain"); // ❌ hiányzik a res.end() });
Nincs hibaüzenet — a szerver csendben vár, a böngésző pedig pörög, amíg
időtúllépésre nem fut. Parancssorból kipróbálva ugyanez: a curl egyszerűen
nem kap választ, és időtúllépéssel áll le.
Ha „nem válaszol a szerver”, először a res.end() hívást keresd
meg. Minden ágon le kell zárni a választ — a hibaágakon is.
Ha elmarad a res.end(), a kérés örökre lóg (nincs
hibaüzenet). Ha kétszer válaszolsz,
ERR_HTTP_ hibát kapsz, és a szerver le is áll.
A kettő ugyanannak a szabálynak a két oldala: egy kérésre pontosan egy válasz jár.
res.setHeader("Content-Type", "text/plain"); // ❌ hiányzik a charset res.end("Árvíztűrő tükörfúrógép");
Megoldás: "text/plain; charset=utf-8".
const oldal = cim.searchParams.get("page"); // "2" — SZÖVEG! if (oldal === 2) { /* soha nem fut le */ } // ❌ if (Number(oldal) === 2) { /* így jó */ } // ✅
Kattints a parancsokra, és nézd meg, mit ír ki a Node. Ez az egyetlen hely a fejezetben, ahol terminált szimulálunk — a szerver naplósorait máshogy nehéz megmutatni.
A szimuláció ugyanazt a kimenetet mutatja, amit a saját gépeden is látnál.
Elméleti
Döntsd el mindegyikről:
console.log("indul") a fájl legelejéncreateServer callbackjében lévő kódlisten() második paramétereként átadott függvényconst termekek = [] deklaráció a fájl tetejénA kliens ezt a címet hívja meg:
http://localhost:3000/api/products/42?reszletes=true#ertekelesek
Mi lesz a req.url értéke? Mi nem kerül bele, és miért?
Mi a hiba, és mi lesz a tünete?
if (req.url === "/api/products") { res.end(JSON.stringify(termekek)); } if (req.url === "/api/categories") { res.end(JSON.stringify(kategoriak)); } res.statusCode = 404; res.end("Nincs ilyen");
Írd le saját szavaiddal, mi az az öt dolog, amit az Express elvesz a válladról a
http modulhoz képest. Mindegyikre mondj egy mondatos indoklást!
Miért nem adja oda a Node egyben a kérés törzsét? Gondolj egy 2 GB-os fájlfeltöltésre! Írj 4-5 mondatot.
Gyakorlati
Készíts egy szervert, amely minden kérésre a "Szia, <a te neved>!"
szöveget adja vissza, helyes Content-Type fejléccel (ékezetekkel!).
Indítsd node --watch-csal, és bizonyítsd be, hogy mentéskor újraindul.
Bővítsd ki három végponttal:
GET / → rövid üdvözlő szövegGET /api/idopont → JSON az aktuális idővel (new Date().toISOString() )404, egységes JSON hibaformátumbanPróbáld ki mindhármat REST Clienttel, és jegyezd fel a státuszkódokat!
Készíts egy GET /api/osszead?a=5&b=3 végpontot, amely JSON-ban adja
vissza az összeget. Kezeld azt is, ha valamelyik paraméter hiányzik vagy nem szám — ilyenkor
400-as státuszkóddal válaszolj!
Készíts egy POST /api/uzenetek végpontot, amely:
JSON.parse-szal feldolgozza (try/catch-csel, hibás JSON esetén 400),201 státuszkóddal visszaadja a létrehozott elemet.Egészítsd ki egy GET /api/uzenetek végponttal, amely visszaadja
az összeset!
http moduljával néhány sorból
indítható HTTP-szerver — nem kell hozzá semmilyen csomag.createServer callbackje minden kérésnél lefut;
a listen callbackje egyszer, induláskor. A szerver
ezután fut és vár, nem lép ki.req.method, a req.url és a
req.headers érdekel minket. A req.url
nem teljes URL: csak az útvonalat és a query stringet tartalmazza.Content-Type fejléc, törzs — és a
végén kötelező a res.end().URL osztállyal
dolgozzuk fel; minden paraméter szövegként érkezik.for await
ciklussal olvassuk be. req.body itt még nincs.| Fogalom | Jelentés |
|---|---|
http modul | A Node beépített modulja HTTP-szerver és -kliens készítéséhez |
node: előtag | Egyértelműsíti, hogy beépített modulról van szó, nem npm-csomagról |
createServer(fv) | Szerverobjektumot hoz létre; a függvény minden kérésnél lefut |
listen(port, fv) | Elindítja a szervert; a függvény egyszer fut le |
req (request) | A beérkező kérés objektuma |
res (response) | A válasz objektuma, amit mi töltünk meg |
res.end() | Elküldi a törzset és lezárja a választ |
res.writeHead() | Státuszkód és fejlécek beállítása egy lépésben |
| Stream | Darabokban érkező adat — a kérés törzse is ilyen |
| Chunk (darab) | A streamből egyszerre megérkező adatrészlet |
URL osztály | Beépített URL-feldolgozó; pathname és searchParams tulajdonságokkal |
EADDRINUSE | Hibakód: a port már foglalt |