Vezérlőpult
2. Az első HTTP-szerver Node-dal · Express alapok

Express alapok · 2. tananyag

Az első HTTP-szerver Node-dal

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.

2 tanóraBeépített http modulÁtvezetés az Expresshez

A fejezet végén tudni fogod

1Az első szerver — hat sor

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.

A 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.

Mi micsoda?

ElemMit 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
A TANANYAGBAN 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.

2Mi fut le egyszer, mi minden kérésnél?

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.");
});
INDULÁSKOR — EGYSZER 1. a fájl teteje 2. a listen() callbackje …és ezután a program VÁR GET / 1. kérés a callback lefut (3.) GET /favicon.ico 2. kérés — ezt a böngésző küldi a callback ÚJRA lefut (3.) GET /api/products 3. kérés a callback ÚJRA lefut (3.) idő ↓
1. ábra — A szerver nem „fut le”, hanem fut: elindul, majd vár a kérésekre.

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.
MIÉRT KÉTSZER?

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.

Egyszer vagy minden kérésnél?

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.

1 / 6Helyes: 0
…

3A kérés: mit tudunk meg?

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

Amire figyelj

TulajdonságMit 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
EMLÉKEZTETŐ AZ 1. BLOKK 2. FEJEZETÉBŐL

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.

4A válasz: hogyan válaszolunk?

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
});
A KÉSZ VÁLASZ HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sat, 29 Aug 2026 14:59:32 GMT Connection: keep-alive Keep-Alive: timeout=5 a szürkéket a Node teszi hozzá magától ÜRES SOR {"uzenet": "Szia!"} res.statusCode = 200 alapból is 200 — hibánál nekünk kell átírni res.setHeader("Content-Type", …) enélkül a böngésző találgat, az ékezet elromlik res.end(JSON.stringify(…)) KÖTELEZŐ — enélkül a kérés örökre lóg MIÉRT KELL A charset=utf-8? Enélkül az á, é, ő, ű kérdőjelként jelenhet meg.
2. ábra — Három sor, három rész. A szürke fejléceket a Node teszi hozzá magától.
SorMit csinál?Mi történne nélküle?
res.statusCode = 200Beállítja a státuszkódotAlapbó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álaszA 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álasztA kérés soha nem fejeződne be — a böngésző csak pörögne

Rövidebb írásmód

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ó.

5Útvonalak — kézzel

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"));
MIÉRT KELL A 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.

És a query string?

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"
ElemMit 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.get("kulcs") Egy query paraméter értéke — mindig szövegként!
VESZÉLY — MINDEN QUERY PARAMÉTER SZÖVEG

A searchParams.get("page") 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.get("page")).

Ami itt már fájni kezd

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);
  // …
});

6A kérés törzse — darabokban érkezik

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.

IDŐ → { "name": "Tes 1. darab zt", "price" 2. darab : 100 } 3. darab VÉGE nincs több darab for await (const darab of req) { torzs += darab; } JSON.parse(torzs) A ciklus megvárja mindegyik darabot. Amíg fut, a Node más kéréseket is ki tud szolgálni. A teljes szöveg csak a ciklus után áll rendelkezésre — előtte hiányos JSON-t kapnánk.
3. ábra — A törzs darabokban érkezik; a 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 } }

Miért nem egyszerűen 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 EXPRESSBEN EZ EGY SOR LESZ

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.

A régi írásmód

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);
  // …
});

7Mi hiányzik még ebből?

Írjuk össze, mi mindent kellene még megvalósítanunk egy valódi API-hoz:

FeladatMennyi 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ásaMinden POST/PUT végpontnál újra
Hibás JSON kezelésetry/catch a JSON.parse köré, mindenhol
Egységes 404-es válaszKézzel, a lánc végén
Központi hibakezelésNincs — minden try/catch a mi dolgunk
Statikus fájlok kiszolgálásaFá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.stringify
CORS, tömörítés, biztonsági fejlécekMind 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.

Ugyanaz a feladat kétféleképpen

„Adj vissza egy JSON-listát a GET /api/products címen”:

http modul15 SOR
import 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);
Express6 SOR
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.stringify 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.

8Gyakori hibák

EADDRINUSE — a port foglalt

Ez 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ésVá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ásKeresd 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
TIPP

Ezért érdemes a portot változóból venni: const PORT = process.env.PORT || 3000; — így egy .env fájlból vagy parancssorból is átállítható, kódmódosítás nélkül.

A böngésző pörög, de nem jön válasz

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.

A KÉT TÜNET KÜLÖNBSÉGE

Ha elmarad a res.end(), a kérés örökre lóg (nincs hibaüzenet). Ha kétszer válaszolsz, ERR_HTTP_HEADERS_SENT 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.

Az ékezetek krix-kraxok

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".

A query paraméter szövegként érkezik

const oldal = cim.searchParams.get("page");   // "2" — SZÖVEG!
if (oldal === 2) { /* soha nem fut le */ }    // ❌
if (Number(oldal) === 2) { /* így jó */ }     // ✅

Egy szerver élete a terminálban

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.

terminal — webshop-api
Válassz egy parancsot alább.

A szimuláció ugyanazt a kimenetet mutatja, amit a saját gépeden is látnál.

9Feladatok

Elméleti

F1⭐Egyszer vagy minden kérésnél?

Döntsd el mindegyikről:

  • a) A console.log("indul") a fájl legelején
  • b) A createServer callbackjében lévő kód
  • c) A listen() második paramétereként átadott függvény
  • d) Egy const termekek = [] deklaráció a fájl tetején
F2⭐Mit tartalmaz?

A 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?

F3⭐⭐Hibakeresés

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");
F4⭐⭐Összehasonlítás

Í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!

F5⭐⭐⭐Miért stream?

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

G1⭐Az első szerver

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.

G2⭐⭐Három végpont

Bővítsd ki három végponttal:

  • GET / → rövid üdvözlő szöveg
  • GET /api/idopont → JSON az aktuális idővel (new Date().toISOString())
  • minden más → 404, egységes JSON hibaformátumban

Próbáld ki mindhármat REST Clienttel, és jegyezd fel a státuszkódokat!

G3⭐⭐Query paraméterek

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!

G4⭐⭐⭐POST-végpont

Készíts egy POST /api/uzenetek végpontot, amely:

  • beolvassa a kérés törzsét,
  • JSON.parse-szal feldolgozza (try/catch-csel, hibás JSON esetén 400),
  • eltárolja egy tömbben,
  • 201 státuszkóddal visszaadja a létrehozott elemet.

Egészítsd ki egy GET /api/uzenetek végponttal, amely visszaadja az összeset!

10Önellenőrző kvíz

0 / 10 megválaszolva

11Összegzés

  1. A Node beépített http moduljával néhány sorból indítható HTTP-szerver — nem kell hozzá semmilyen csomag.
  2. A 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.
  3. A kérésből a 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.
  4. A válaszhoz három dolgot állítunk be: státuszkód, Content-Type fejléc, törzs — és a végén kötelező a res.end().
  5. A query stringet a beépített URL osztállyal dolgozzuk fel; minden paraméter szövegként érkezik.
  6. A kérés törzse darabokban érkezik, ezért for await ciklussal olvassuk be. req.body itt még nincs.
  7. Az útvonalkezelés, a törzs-feldolgozás, a 404, a hibakezelés és a statikus fájlok mind kézi munka lennének — ezért használunk keretrendszert. A következő fejezettől Express.

12Fogalomtár

FogalomJelentés
http modulA Node beépített modulja HTTP-szerver és -kliens készítéséhez
node: előtagEgyé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
StreamDarabokban érkező adat — a kérés törzse is ilyen
Chunk (darab)A streamből egyszerre megérkező adatrészlet
URL osztályBeépített URL-feldolgozó; pathname és searchParams tulajdonságokkal
EADDRINUSEHibakód: a port már foglalt
← Előző tananyag 1. Aszinkron JavaScript