Wyszukiwanie w witrynie

Jak utworzyć serwer internetowy w Node.js przy użyciu modułu HTTP


Wprowadzenie

Kiedy przeglądasz stronę internetową w przeglądarce, wysyłasz żądanie do innego komputera w internecie, który następnie przesyła Ci stronę internetową jako odpowiedź. Komputer, z którym się komunikujesz za pośrednictwem internetu, to serwer WWW. Serwer WWW odbiera żądania HTTP od klienta, takiego jak Twoja przeglądarka, i udziela odpowiedzi HTTP, jak strona HTML lub JSON z API.

Moduł http w Node.js zapewnia wszystko, czego potrzebujesz do stworzenia serwera internetowego bez zewnętrznych zależności. Obsługuje przychodzące żądania, analizuje nagłówki HTTP, zarządza połączeniami i wysyła odpowiedzi — wszystko za pomocą prostych interfejsów API w JavaScript.

Moduł http jest wbudowanym rozwiązaniem Node.js do tworzenia serwerów i klientów HTTP. W przeciwieństwie do frameworków takich jak Express.js, jest częścią rdzenia Node.js, co oznacza, że nie wymaga instalacji ani dodatkowych zależności. Dzięki temu jest idealny dla lekkich aplikacji, nauki podstaw HTTP lub gdy chcesz mieć pełną kontrolę nad obsługą żądań.

Do zwrócenia strony internetowej przez serwer potrzebne jest dużo oprogramowania. Zazwyczaj dzieli się ono na dwie kategorie: frontend i backend. Kod front-endowy zajmuje się tym, jak prezentowana jest zawartość, na przykład kolorem paska nawigacyjnego i stylizacją tekstu. Kod back-endowy zajmuje się tym, jak dane są wymieniane, przetwarzane i przechowywane. Kod, który obsługuje żądania sieciowe z przeglądarki lub komunikuje się z bazą danych, jest głównie zarządzany przez kod back-endowy.

Node.js pozwala programistom używać JavaScript do pisania kodu back-end, mimo że tradycyjnie był on używany w przeglądarce do pisania kodu front-end. Posiadanie zarówno frontendu, jak i backendu razem w ten sposób zmniejsza nakład pracy potrzebny do stworzenia serwera internetowego, co jest głównym powodem, dla którego Node.js jest popularnym wyborem do pisania kodu back-end.

Ten samouczek pokazuje, jak tworzyć gotowe do produkcji serwery internetowe za pomocą modułu http, który jest wbudowany w Node.js. Zbudujesz serwery, które obsługują routing, analizują treści żądań, udostępniają pliki statyczne, implementują właściwe obsługiwanie błędów oraz działają zarówno z składnią CommonJS, jak i ES Module.

Uprość wdrażanie aplikacji Node.js za pomocą platformy App Platform Linux-Console.net. Wdrażaj Node bezpośrednio z GitHub w kilka minut.

Najważniejsze wnioski

Zanim zaczniesz, oto co osiągniesz:

  • Tworzenie serwerów HTTP z użyciem wbudowanego modułu http w Node.js bez frameworków:
    Nauczysz się, jak uruchomić w pełni funkcjonalny serwer HTTP, korzystając wyłącznie z wbudowanego modułu http w Node.js. Oznacza to unikanie zewnętrznych zależności czy frameworków, co pozwala na głębsze zrozumienie, jak działają serwery na niskim poziomie, a także umożliwia bardziej lekkie i elastyczne implementacje dla Twoich zastosowań.

  • Obsługuj różne typy treści (JSON, HTML, CSV, pliki statyczne) z właściwymi nagłówkami:
    Dowiesz się, jak pracować z wieloma typami treści, odpowiadając HTML-em dla stron internetowych, JSON-em dla API, plikami CSV lub obsługując zasoby statyczne, takie jak obrazy i skrypty. Zrozumiesz, jak prawidłowo ustawiać nagłówki HTTP, takie jak Content-Type, aby przeglądarki i klienci poprawnie interpretowali Twoje odpowiedzi, zapewniając płynne doświadczenia użytkowników.

  • Implementuj routing za pomocą analizowania URL żądania i instrukcji switch:
    Routing jest podstawą dla serwerów internetowych, pozwalając na obsługę różnych logik dla różnych URL-i i punktów końcowych. Zobaczysz techniki analizowania przychodzących URL-i żądań i strukturyzowania logiki serwera (na przykład za pomocą instrukcji switch), aby zapewnić niestandardowe odpowiedzi w zależności od ścieżek i metod HTTP, symulując podstawy działania większych frameworków w tle.

  • Przetwarzanie żądań POST poprzez analizowanie treści żądań i obsługę różnych typów danych:
    Obsługa danych przesyłanych przez użytkowników jest kluczowa dla każdej interaktywnej aplikacji. Nauczysz się, jak obsługiwać żądania POST, w tym jak odczytywać i analizować treść żądania jako surowe dane, JSON lub inne formaty. Ta sekcja obejmuje buforowanie przychodzących danych, ich bezpieczne analizowanie oraz odpowiadanie zgodnie z otrzymanym ładunkiem, dostarczając podstaw do interakcji użytkownika lub tworzenia interfejsów API.

  • Efektywne serwowanie statycznych plików z systemu plików:
    Wiele serwerów sieciowych musi obsługiwać statyczne zasoby – obrazy, arkusze stylów, pliki JavaScript – bezpośrednio dla użytkowników. Dowiesz się, jak czytać pliki z dysku i wysyłać je efektywnie przez HTTP, odpowiedzialnie zarządzać ścieżkami plików, aby unikać problemów z bezpieczeństwem, oraz poznasz optymalizacje, takie jak strumieniowanie plików, aby poprawić wydajność przy większych plikach.

  • Zaimplementuj obsługę błędów z odpowiednimi kodami statusu HTTP i rejestrowaniem zdarzeń:
    Solidny serwer musi obsługiwać błędy w sposób elegancki. Zobaczysz, jak wykrywać, rejestrować i odpowiadać na błędy — takie jak błędne żądania czy brakujące pliki — używając standardowych kodów statusu HTTP, takich jak 400, 404 i 500. Nauczysz się także podstawowych praktyk rejestrowania, aby problemy były śledzalne, a użytkownicy otrzymywali informacyjne komunikaty o błędach bez ujawniania poufnych informacji.

  • Używaj nowoczesnego JavaScript zarówno z składnią CommonJS (require), jak i ES Modules (import):
    Node.js obsługuje dwa systemy modułów: CommonJS i ES Modules. Będziesz ćwiczyć używanie zarówno require(), jak i import, rozumiejąc, kiedy i jak korzystać z każdego stylu. To nie tylko zapewnia kompatybilność w różnych projektach Node.js, ale także pomaga pisać nowoczesny, czysty i łatwy w utrzymaniu kod JavaScript.

  • Stosuj najlepsze praktyki produkcyjne, w tym buforowanie plików, odzyskiwanie po błędach i kwestie bezpieczeństwa:
    Na koniec omówisz kluczowe najlepsze praktyki dotyczące wdrażania serwerów Node.js w środowiskach produkcyjnych. Obejmuje to wykorzystanie buforowania plików w celu zwiększenia wydajności, wdrażanie strategii odzyskiwania po błędach, aby serwer był odporny, a także stosowanie podstawowych środków bezpieczeństwa (takich jak oczyszczanie danych wejściowych i odpowiednie nagłówki HTTP), aby chronić aplikacje i użytkowników.

Rozumienie modułu HTTP

Moduł http jest niskopoziomowym interfejsem Node.js do komunikacji HTTP. Zapewnia:

  • Tworzenie serwera: http.createServer() tworzy instancję serwera HTTP
  • Obsługa żądań: Nadejście żądań reprezentowane jest przez obiekty IncomingMessage
  • Zarządzanie odpowiedziami: Odpowiedzi wychodzące używają obiektów ServerResponse
  • Obsługa strumieniowania: Zarówno żądania, jak i odpowiedzi są strumieniami, co umożliwia efektywny transfer danych

Dlaczego warto używać modułu HTTP zamiast Express? Chociaż Express.js oferuje wygodne abstrakcje, natywny moduł http daje pełną kontrolę, mniejsze rozmiary paczek i głębokie zrozumienie podstaw HTTP. To fundament, na którym budowane są frameworki. Dla prostych API, mikrousług lub celów edukacyjnych często jest to wszystko, czego potrzebujesz.

Moduł jest sterowany zdarzeniami: gdy przychodzi żądanie, serwer wywołuje zdarzenie 'request' i uruchamia Twoją funkcję nasłuchującą na żądania. Taki projekt umożliwia Node.js efektywne obsługiwanie wielu jednoczesnych połączeń na jednym wątku dzięki pętli zdarzeń.

Podstawowe pojęcia do zrozumienia:

  • Wejście/wyjście nieblokujące: Moduł http korzysta z pętli zdarzeń Node.js do obsługi wielu połączeń bez tworzenia osobnych wątków dla każdego żądania. Sprawia to, że jest on bardzo wydajny w operacjach zależnych od I/O.
  • Strumienie: Obiekty żądań i odpowiedzi dziedziczą po strumieniach Node.js. Oznacza to, że duże dane mogą być przetwarzane w kawałkach bez konieczności wczytywania wszystkiego do pamięci—jest to kluczowe przy obsłudze przesyłania plików lub udostępnianiu dużych plików.
  • Nagłówki i kody statusu: Moduł daje bezpośrednią kontrolę nad nagłówkami HTTP (res.setHeader()) i kodami statusu (res.writeHead()), umożliwiając precyzyjne operacje na protokole HTTP.
  • Brak magicznego routingu: W przeciwieństwie do frameworków, nie ma wbudowanego routingu. Ręcznie analizujesz req.url i req.method, aby określić, jak odpowiedzieć — co daje pełną przejrzystość w tym, jak routing faktycznie działa.

Kiedy wywołujesz http.createServer(), Node.js tworzy w tle serwer TCP, który nasłuchuje nadchodzących połączeń, analizuje wiadomości protokołu HTTP i generuje zdarzenia, które Twój kod może obsłużyć. Zrozumienie tej niskopoziomowej operacji pomaga w debugowaniu problemów i podejmowaniu świadomych decyzji architektonicznych.

Wymagania wstępne

  • Upewnij się, że Node.js jest zainstalowany na Twoim komputerze deweloperskim. Ten samouczek działa z Node.js 20.x lub 22.x LTS (zalecane). Aby zainstalować Node.js na macOS lub Ubuntu, postępuj zgodnie z instrukcjami w Jak zainstalować Node.js i utworzyć lokalne środowisko deweloperskie na macOS lub Jak zainstalować Node.js na Ubuntu.

  • Platforma Node.js obsługuje tworzenie serwerów internetowych od razu po instalacji. Znajomość podstaw Node.js jest przydatna. Zapoznaj się z naszym przewodnikiem Jak napisać i uruchomić swój pierwszy program w Node.js.

  • Programowanie asynchroniczne jest używane w niektórych sekcjach. Jeśli jesteś nowy w kodzie asynchronicznym lub w module fs, zobacz Jak pisać kod asynchroniczny w Node.js.

  • W sekcjach skoncentrowanych na produkcji znajomość zarządzania procesami w systemie Linux (systemd/PM2) oraz serwerów proxy (takich jak Nginx) będzie pomocna przy stosowaniu wytycznych dotyczących wdrożenia.

Krok 1 — Tworzenie podstawowego serwera HTTP

Zbuduj minimalny serwer HTTP, który odpowiada na każde żądanie zwykłym tekstem. Ta podstawa obowiązuje niezależnie od tego, czy używasz Express, czy tworzysz od podstaw. Gdy Node.js wywołuje http.createServer(), tworzy serwer TCP nasłuchujący na porcie. Każde nadchodzące połączenie wywołuje Twoją funkcję nasłuchującą żądań.

Skonfiguruj folder projektu:

mkdir first-servers

cd first-servers

Teraz stwórz plik, w którym będzie znajdować się kod:

touch hello.js

Otwórz plik w edytorze tekstu. Użyjemy nano, ponieważ jest dostępny w terminalu:

nano hello.js

Zaczynamy od załadowania modułu http, który jest standardowym modułem we wszystkich instalacjach Node.js. Dodaj następującą linię do hello.js:

const http = require("http");

Moduł http zawiera funkcję do tworzenia serwera, którą zobaczymy później. Jeśli chcesz dowiedzieć się więcej o modułach w Node.js, sprawdź nasz artykuł Jak stworzyć moduł Node.js.

Naszym kolejnym krokiem będzie zdefiniowanie dwóch stałych: hosta i portu, do którego będzie przypisany nasz serwer:

...
const host = 'localhost';
const port = 8000;

Jak wspomniano wcześniej, serwery internetowe przyjmują żądania od przeglądarek i innych klientów. Możemy wchodzić w interakcję z serwerem internetowym, wpisując nazwę domeny, która jest tłumaczona na adres IP przez serwer DNS. Adres IP to unikalna sekwencja liczb identyfikująca urządzenie w sieci, takiej jak internet. Aby uzyskać więcej informacji na temat pojęć związanych z nazwami domen, zapoznaj się z naszym artykułem Wprowadzenie do terminologii, składników i pojęć DNS.

Wartość localhost to specjalny prywatny adres, którego komputery używają do odwoływania się do siebie samego. Zazwyczaj jest to odpowiednik wewnętrznego adresu IP 127.0.0.1 i jest dostępny tylko dla lokalnego komputera, a nie dla żadnych sieci lokalnych, do których jesteśmy podłączeni, ani dla internetu.

Port to numer, którego serwery używają jako punkt końcowy lub „drzwi” do naszego adresu IP. W naszym przykładzie użyjemy portu 8000 dla naszego serwera WWW. Porty 8080 i 8000 są zazwyczaj używane jako domyślne porty w trakcie tworzenia oprogramowania i w większości przypadków programiści używają ich zamiast innych portów dla serwerów HTTP.

Kiedy przypiszemy nasz serwer do tego hosta i portu, będziemy mogli uzyskać dostęp do naszego serwera, odwiedzając http://localhost:8000 w lokalnej przeglądarce.

Dodajmy specjalną funkcję, którą w Node.js nazywamy nasłuchem żądań. Funkcja ta ma za zadanie obsługiwać przychodzące żądania HTTP i zwracać odpowiedzi HTTP. Funkcja ta musi mieć dwa argumenty: obiekt żądania i obiekt odpowiedzi. Obiekt żądania przechowuje wszystkie dane przychodzącego żądania HTTP. Obiekt odpowiedzi służy do zwracania odpowiedzi HTTP przez serwer.

Chcemy, aby nasz pierwszy serwer zwracał tę wiadomość za każdym razem, gdy ktoś go odwiedzi: "Mój pierwszy serwer!".

Dodajmy teraz tę funkcję:

...

const requestListener = function (req, res) {
  res.writeHead(200);
  res.end("My first server!");
};

Funkcja zazwyczaj byłaby nazywana w oparciu o to, co robi. Na przykład, jeśli stworzyliśmy funkcję nasłuchującą żądania, aby zwrócić listę książek, prawdopodobnie nazwalibyśmy ją listBooks(). Ponieważ jest to przykład, użyjemy ogólnej nazwy requestListener.

Wszystkie funkcje nasłuchujące żądań w Node.js przyjmują dwa argumenty: req i res (możemy je nazwać inaczej, jeśli chcemy). Żądanie HTTP wysyłane przez użytkownika jest przechwytywane w obiekcie Request, który odpowiada pierwszemu argumentowi, req. Odpowiedź HTTP, którą zwracamy użytkownikowi, jest tworzona poprzez interakcję z obiektem Response w drugim argumencie, res.

Pierwsza linia res.writeHead(200); ustawia kod statusu HTTP odpowiedzi. Kody statusu HTTP wskazują, jak dobrze żądanie HTTP zostało obsłużone przez serwer. W tym przypadku kod statusu 200 odpowiada "OK". Jeśli interesuje Cię poznanie różnych kodów HTTP, które mogą zwracać Twoje serwery internetowe, wraz z ich znaczeniem, nasz przewodnik Jak Rozwiązywać Najczęstsze Błędy HTTP będzie dobrym miejscem na początek.

Następna linia funkcji, res.end("Mój pierwszy serwer!");, wysyła odpowiedź HTTP z powrotem do klienta, który ją zażądał. Ta funkcja zwraca wszelkie dane, które serwer musi odesłać. W tym przypadku zwraca dane w postaci tekstu.

W końcu możemy teraz utworzyć nasz serwer i skorzystać z naszego nasłuchiwacza żądań:

...

const server = http.createServer(requestListener);
server.listen(port, host, () => {
  console.log(`Server is running on http://${host}:${port}`);
});

Zapisz i wyjdź z nano, naciskając CTRL+X.

W pierwszej linii tworzymy nowy obiekt server za pomocą funkcji createServer() modułu http. Ten serwer przyjmuje żądania HTTP i przekazuje je do naszej funkcji requestListener().

Po utworzeniu serwera musimy go powiązać z adresem sieciowym. Robimy to za pomocą metody server.listen(). Przyjmuje ona trzy argumenty: port, host oraz funkcję zwrotną, która uruchamia się, gdy serwer zaczyna nasłuchiwać.

Wszystkie te argumenty są opcjonalne, ale warto jawnie określić, którego portu i hosta chcemy użyć dla serwera internetowego. Podczas wdrażania serwerów internetowych w różnych środowiskach konieczne jest poznanie portu i hosta, na którym działają, aby skonfigurować równoważenie obciążenia lub alias DNS.

Funkcja zwrotna zapisuje komunikat w naszej konsoli, dzięki czemu możemy wiedzieć, kiedy serwer zaczął nasłuchiwać połączeń.

Oto kompletny plik hello.js z komentarzami wstawionymi w kod, wyjaśniającymi każdą część:

// Import the built-in http module - no installation needed
const http = require("http");

// Define where the server will listen
const host = 'localhost';  // Only accessible from this machine
const port = 8000;          // Common development port

// Request listener: handles every incoming HTTP request
const requestListener = function (req, res) {
    // Set HTTP status code to 200 (OK)
    res.writeHead(200);
    
    // Send the response body and close the connection
    res.end("My first server!");
};

// Create the server with our request listener
const server = http.createServer(requestListener);

// Start listening for connections
server.listen(port, host, () => {
    console.log(`Server is running on http://${host}:${port}`);
});

Ten kod tworzy funkcjonalny serwer HTTP w mniej niż 20 linijkach. Za każdym razem, gdy klient się łączy – niezależnie od URL czy metody HTTP – serwer odpowiada „Mój pierwszy serwer!” i zwraca kod statusu 200.

Uwaga: Chociaż requestListener() nie korzysta z obiektu req, wciąż musi on być pierwszym argumentem funkcji.

Mając mniej niż piętnaście linii kodu, mamy teraz serwer internetowy. Zobaczmy go w akcji i przetestujmy od początku do końca, uruchamiając program:

node hello.js

W konsoli zobaczymy ten wynik:

Server is running on http://localhost:8000

Zauważ, że monit znika. Dzieje się tak, ponieważ serwer Node.js to proces działający w tle przez dłuższy czas. Kończy działanie tylko w przypadku wystąpienia błędu, który powoduje jego awarię i zamknięcie, lub jeśli sami zatrzymamy działający proces Node.js serwera.

W osobnym oknie terminala będziemy komunikować się z serwerem za pomocą cURL, narzędzia wiersza poleceń do przesyłania danych do i z sieci. Wprowadź polecenie, aby wykonać żądanie HTTP GET do naszego działającego serwera:

curl http://localhost:8000

Gdy naciśniemy ENTER, nasz terminal wyświetli następujące dane wyjściowe:

My first server!

Właśnie uruchomiliśmy serwer i otrzymaliśmy pierwszą odpowiedź z serwera.

Rozbijmy na części to, co się stało, gdy testowaliśmy nasz serwer. Używając cURL, wysłaliśmy żądanie GET do serwera pod adresem http://localhost:8000. Nasz serwer Node.js nasłuchiwał połączeń z tego adresu. Serwer przekazał to żądanie do funkcji requestListener(). Funkcja zwróciła dane tekstowe ze statusem 200. Serwer następnie odesłał tę odpowiedź z powrotem do cURL, który wyświetlił wiadomość w naszym terminalu.

W środowisku produkcyjnym zazwyczaj nie wiążesz się z localhost — wiążesz się z 0.0.0.0, aby akceptować połączenia z dowolnego interfejsu sieciowego. Jednak w przypadku lokalnego rozwoju localhost jest bezpieczniejsze i uniemożliwia zewnętrzny dostęp do Twojego serwera deweloperskiego. Ponadto, jeśli port 8000 jest już używany, otrzymasz błąd EADDRINUSE — po prostu wybierz inny port lub zatrzymaj proces korzystający z tego portu za pomocą lsof -i :8000 (macOS/Linux).

Zanim przejdziemy dalej, zakończmy działanie naszego serwera, naciskając CTRL+C. To przerywa wykonywanie serwera i przywraca nas do wiersza poleceń.

W większości stron internetowych, które odwiedzamy, lub API, których używamy, odpowiedzi serwera rzadko są w czystym tekście. Zwykle otrzymujemy strony HTML oraz dane JSON jako powszechne formaty odpowiedzi. W kolejnym kroku nauczymy się, jak zwracać odpowiedzi HTTP w popularnych formatach danych, które napotykamy w sieci.

Jeśli chcesz nauczyć się, jak wykonywać żądania HTTP z Node.js (po stronie klienta), sprawdź nasz przewodnik Jak utworzyć klienta HTTP przy użyciu Core HTTP w Node.js.

Krok 2 — Zwracanie różnych rodzajów treści

Odpowiedź, którą zwracamy z serwera internetowego, może mieć różne formaty. Wcześniej wspomniano o JSON i HTML, a także możemy zwracać inne formaty tekstowe, takie jak XML i CSV. Wreszcie, serwery internetowe mogą zwracać dane niebędące tekstem, takie jak pliki PDF, skompresowane pliki, audio i wideo.

W tym artykule, oprócz zwykłego tekstu, który właśnie zwróciliśmy, dowiesz się, jak zwracać następujące typy danych:

  • JSON
  • CSV
  • HTML

Te trzy typy danych są wszystkie oparte na tekście i są popularnymi formatami do dostarczania treści w sieci. Wiele języków i narzędzi do programowania po stronie serwera obsługuje zwracanie tych różnych typów danych. W kontekście Node.js musimy zrobić dwie rzeczy:

  1. Ustaw nagłówek Content-Type w naszych odpowiedziach HTTP z odpowiednią wartością.
  2. Upewnij się, że res.end() otrzymuje dane w odpowiednim formacie.

Zobaczmy to w działaniu na kilku przykładach. Kod, który będziemy pisać w tej sekcji i w kolejnych, jest bardzo podobny do kodu, który napisaliśmy wcześniej. Większość zmian dotyczy funkcji requestListener(). Stwórzmy pliki z tym „kodem szablonowym”, aby ułatwić śledzenie kolejnych sekcji.

Utwórz nowy plik o nazwie html.js. Ten plik będzie później używany do zwracania tekstu HTML w odpowiedzi HTTP. Wstawimy tutaj kod szablonu i skopiujemy go na inne serwery, które zwracają różne typy.

W terminalu wpisz następujące polecenie:

touch html.js

Teraz otwórz ten plik w edytorze tekstu:

nano html.js

Skopiujmy „kod szablonu”. Wprowadź go w nano:

const http = require("http");

const host = 'localhost';
const port = 8000;

const requestListener = function (req, res) {};

const server = http.createServer(requestListener);
server.listen(port, host, () => {
    console.log(`Server is running on http://${host}:${port}`);
});

Zapisz i zamknij html.js za pomocą CTRL+X, a następnie wróć do terminala.

Teraz skopiujmy ten plik do dwóch nowych plików. Pierwszy plik będzie służył do zwracania danych CSV w odpowiedzi HTTP:

cp html.js csv.js

Drugi plik zwróci odpowiedź w formacie JSON na serwerze:

cp html.js json.js

Pozostałe pliki będą przeznaczone na późniejsze ćwiczenia:

cp html.js htmlFile.js

cp html.js routes.js

Jesteśmy teraz gotowi, aby kontynuować nasze ćwiczenia. Zacznijmy od zwracania JSON-a.

Serwowanie JSON

JavaScript Object Notation, powszechnie nazywane JSON, to tekstowy format wymiany danych. Jak sama nazwa wskazuje, wywodzi się on z obiektów JavaScript, jednak jest niezależny od języka, co oznacza, że można go używać w każdym języku programowania, który potrafi przetwarzać jego składnię.

JSON jest powszechnie używany przez interfejsy API do przyjmowania i zwracania danych. Jego popularność wynika z mniejszego rozmiaru przesyłanych danych w porównaniu do wcześniejszych standardów wymiany danych, takich jak XML, a także z narzędzi, które umożliwiają programom ich analizowanie bez nadmiernego wysiłku. Jeśli chcesz dowiedzieć się więcej o JSON, możesz przeczytać nasz przewodnik Jak pracować z JSON w JavaScripcie.

Otwórz plik json.js za pomocą nano:

nano json.js

Podając JSON, musisz ustawić nagłówek Content-Type na application/json. Bez tego przeglądarki i klienci API traktują odpowiedź jako zwykły tekst, co powoduje niepowodzenie fetch().json(). Ustaw nagłówki przed wywołaniem writeHead() — po wysłaniu nagłówków nie można ich zmienić.

Powszechny błąd: Wywoływanie res.writeHead(200) przed res.setHeader() blokuje nagłówki. Jeśli spróbujesz ustawić nagłówki po writeHead(), zostaną one zignorowane. Zawsze najpierw wywołuj setHeader(), a dopiero potem writeHead().

Zmodyfikuj funkcję requestListener():

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "application/json");
    res.writeHead(200);
    res.end(`{"message": "This is a JSON response"}`);
};
...

Kod statusu 200 wskazuje sukces. Treść odpowiedzi zawiera poprawny JSON.

Dlaczego używać JSON.stringify() zamiast ręcznego tworzenia ciągów? Ręczne pisanie JSON jest podatne na błędy. Jeśli Twoja wiadomość zawiera cudzysłowy, nowe linie lub znaki specjalne, musisz je ręcznie uciekać. JSON.stringify() robi to automatycznie:

const data = { 
    message: "This is a JSON response",
    timestamp: Date.now(),
    note: 'He said "hello"' // Quotes automatically escaped
};
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(JSON.stringify(data));

Uważaj na odwołania cykliczne: Jeśli Twój obiekt odwołuje się sam do siebie, JSON.stringify() zgłosi błąd: TypeError: Converting circular structure to JSON. Często zdarza się to przy wynikach zapytań do bazy danych. Zawsze weryfikuj strukturę swoich danych.

Co się stanie, jeśli spróbujesz wysłać obiekt bezpośrednio? Moduł http akceptuje tylko ciągi znaków lub obiekty Buffer. Jeśli spróbujesz res.end({ message: "test" }), Node.js wyrzuci: TypeError [ERR_INVALID_ARG_TYPE]: Pierwszy argument musi być typu string lub Buffer. Najpierw musisz przekonwertować obiekty na ciągi JSON.

Sprawdź, czy nagłówek został ustawiony poprawnie: Użyj flagi -i w curl, aby zobaczyć nagłówki odpowiedzi:

curl -i http://localhost:8000

Powinieneś zobaczyć Content-Type: application/json w nagłówkach. Jeśli zobaczysz text/plain lub brak Content-Type, to znaczy, że wywołałeś setHeader() po writeHead() lub całkowicie o tym zapomniałeś.

Typowy scenariusz debugowania: Jeśli Twój frontendowy JavaScript otrzymuje undefined podczas wywoływania fetch('/api').then(r => r.json()), sprawdź nagłówki za pomocą curl -i. Content-Type musi być dokładnie application/json (wrażliwy na wielkość liter dla niektórych rygorystycznych parserów).

Zapisz i zamknij json.js, naciskając CTRL+X. Teraz uruchommy serwer za pomocą polecenia node:

node json.js

W innym terminalu połączmy się z serwerem za pomocą cURL:

curl http://localhost:8000

Gdy naciśniemy ENTER, zobaczymy następujący rezultat:

{"message": "This is a JSON response"}

Teraz udało nam się pomyślnie zwrócić odpowiedź JSON, podobnie jak w przypadku wielu popularnych API, z których korzystamy przy tworzeniu aplikacji. Upewnij się, że zakończysz działanie serwera za pomocą CTRL+C, aby powrócić do standardowego wiersza poleceń. Następnie przyjrzyjmy się innemu popularnemu formatowi zwracania danych: CSV.

Serwowanie CSV

Format pliku Comma Separated Values (CSV) to standard tekstowy, który jest powszechnie używany do udostępniania danych tabelarycznych. W większości przypadków każdy wiersz jest oddzielony nową linią, a każdy element w wierszu jest oddzielony przecinkiem.

W naszym środowisku pracy otwórz plik csv.js za pomocą edytora tekstu:

nano csv.js

Dodajmy następujące linie do naszej funkcji requestListener():

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/csv");
    res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
};
...

Tym razem nasz Content-Type wskazuje, że zwracany jest plik CSV, ponieważ wartość to text/csv. Drugi nagłówek, który dodajemy, to Content-Disposition. Nagłówek ten informuje przeglądarkę, jak wyświetlić dane, szczególnie w przeglądarce lub jako osobny plik.

Kiedy zwracamy odpowiedzi w formacie CSV, większość nowoczesnych przeglądarek automatycznie pobiera plik, nawet jeśli nagłówek Content-Disposition nie został ustawiony. Jednak zwracając plik CSV, powinniśmy nadal dodać ten nagłówek, ponieważ pozwala on ustawić nazwę pliku CSV. W tym przypadku sygnalizujemy przeglądarce, że ten plik CSV jest załącznikiem i powinien zostać pobrany. Następnie informujemy przeglądarkę, że nazwa pliku to oceanpals.csv.

Zapiszmy dane CSV w odpowiedzi HTTP:

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/csv");
    res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
    res.writeHead(200);
    res.end(`id,name,email\n1,Sammy Shark,shark@ocean.com`);
};
...

Zwróć status 200/OK wraz z danymi w formacie CSV. Przecinek oddziela wartości w każdej kolumnie, a znak nowego wiersza (\n) oddziela wiersze. Mamy dwa wiersze: wiersz nagłówka i jeden wiersz danych.

Pułapka przy formatowaniu CSV: Prostym przykładem powyżej działa, ale generowanie plików CSV w środowisku produkcyjnym wymaga odpowiedniego zabezpieczenia danych. Jeśli Twoje dane zawierają przecinki (jak "Smith, John") lub cudzysłowy, mogą one zepsuć format CSV. Poprawne zabezpieczenie CSV polega na umieszczaniu wartości w cudzysłowach oraz podwajaniu wewnętrznych cudzysłowów: "Smith, John" staje się "Smith, ""John""" w pliku CSV. W środowisku produkcyjnym używaj biblioteki takiej jak csv-stringify lub odpowiednio zabezpiecz wartości, aby uniknąć błędów podczas parsowania.

Co się dzieje bez Content-Disposition? Większość przeglądarek pobiera plik, jeśli ustawiony jest Content-Type: text/csv, ale nazwa pliku może odpowiadać ścieżce URL lub być czymś ogólnym. Nagłówek attachment;filename=oceanpals.csv zapewnia, że przeglądarka zapisze go pod określoną nazwą. Bez attachment niektóre przeglądarki próbują wyświetlić CSV bezpośrednio, co często wygląda nieczytelnie.

Przetestujemy ten serwer w przeglądarce. Zapisz csv.js i wyjdź z edytora używając CTRL+X.

Uruchom serwer poleceniem Node.js:

node csv.js

W innym terminalu połączmy się z serwerem za pomocą cURL:

curl http://localhost:8000

Konsola wyświetli to:

id,name,email
1,Sammy Shark,shark@ocean.com

Jeśli przejdziemy w naszej przeglądarce pod adres http://localhost:8000, zostanie pobrany plik CSV. Jego nazwa to oceanpals.csv.

Zakończ działający serwer za pomocą CTRL+C, aby wrócić do standardowego wiersza poleceń terminala.

Po opisaniu zwracania danych w formatach JSON i CSV omówiliśmy dwa popularne przypadki użycia w API. Przejdźmy teraz do sposobu zwracania danych na strony internetowe, które ludzie przeglądają w przeglądarce.

Serwowanie HTML

HTML (HyperText Markup Language) jest najczęściej stosowanym formatem, gdy chcemy, aby użytkownicy wchodzili w interakcję z naszym serwerem za pomocą przeglądarki internetowej. Został stworzony do strukturyzowania treści internetowych. Przeglądarki internetowe są zaprojektowane do wyświetlania treści HTML, jak również wszelkich stylów, które dodajemy za pomocą CSS, innej technologii front-end, która pozwala nam zmieniać wygląd naszych stron internetowych.

Otwórzmy ponownie html.js w naszym edytorze tekstu:

nano html.js

Zmodyfikuj funkcję requestListener(), aby zwracała odpowiedni nagłówek Content-Type dla odpowiedzi HTML:

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/html");
};
...

Teraz zwróćmy użytkownikowi zawartość HTML. Dodaj podświetlone linie do html.js, tak aby wyglądało to w ten sposób:

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/html");
    res.writeHead(200);
    res.end(`<html><body><h1>This is HTML</h1></body></html>`);
};
...

Najpierw dodajemy kod statusu HTTP. Następnie wywołujemy response.end() z argumentem w postaci ciągu znaków zawierającego poprawny HTML. Kiedy uzyskamy dostęp do naszego serwera w przeglądarce, zobaczymy stronę HTML z jednym tagiem nagłówka zawierającym This is HTML.

Zapiszmy i wyjdźmy, naciskając CTRL+X. Teraz uruchommy serwer za pomocą polecenia node:

node html.js

Zobaczymy Server is running on http://localhost:8000, gdy nasz program się uruchomi.

Teraz przejdź do przeglądarki i odwiedź http://localhost:8000. Nasza strona będzie wyglądać tak:

Zakończmy działający serwer przy pomocy CTRL+C i wróćmy do standardowego wiersza poleceń.

HTML jest często zapisywany w pliku, oddzielnie od kodu po stronie serwera, takiego jak nasze programy Node.js. Następnie zobaczmy, jak możemy zwracać odpowiedzi HTML z plików.

Krok 3 — Serwowanie strony HTML z pliku

Możemy udostępniać HTML jako ciągi znaków w Node.js użytkownikowi, ale lepiej jest ładować pliki HTML i udostępniać ich zawartość. Dzięki temu, w miarę jak plik HTML rośnie, nie musimy utrzymywać długich ciągów znaków w naszym kodzie Node.js, co sprawia, że jest on bardziej zwięzły i pozwala nam pracować nad każdym aspektem naszej strony niezależnie. To „rozdzielenie obaw” jest powszechne w wielu konfiguracjach tworzenia stron internetowych, więc warto wiedzieć, jak ładować pliki HTML, aby wspierać to w Node.js.

Aby obsługiwać pliki HTML, ładujemy plik HTML za pomocą modułu fs i używamy jego danych podczas pisania naszej odpowiedzi HTTP.

Najpierw utworzymy plik HTML, który zostanie zwrócony przez serwer internetowy. Utwórz nowy plik HTML:

touch index.html

Teraz otwórz index.html w edytorze tekstu:

nano index.html

Nasza strona internetowa będzie minimalistyczna. Będzie miała pomarańczowe tło i wyświetlała pewien tekst powitalny na środku. Dodaj ten kod do pliku:

<!DOCTYPE html>

<head>
    <title>My Website</title>
    <style>
        *,
        html {
            margin: 0;
            padding: 0;
            border: 0;
        }

        html {
            width: 100%;
            height: 100%;
        }

        body {
            width: 100%;
            height: 100%;
            position: relative;
            background-color: rgb(236, 152, 42);
        }

        .center {
            width: 100%;
            height: 50%;
            margin: 0;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-family: "Trebuchet MS", Helvetica, sans-serif;
            text-align: center;
        }

        h1 {
            font-size: 144px;
        }

        p {
            font-size: 64px;
        }
    </style>
</head>

<body>
    <div class="center">
        <h1>Hello Again!</h1>
        <p>This is served from a file</p>
    </div>
</body>

</html>

Ta pojedyncza strona internetowa wyświetla dwie linie tekstu: Hello Again! oraz This is served from a file. Linie pojawiają się na środku strony, jedna nad drugą. Pierwsza linia tekstu jest wyświetlana jako nagłówek, co oznacza, że będzie duża. Druga linia tekstu będzie nieco mniejsza. Cały tekst będzie biały, a tło strony będzie pomarańczowe.

Chociaż nie jest to tematem tego artykułu ani serii, jeśli jesteś zainteresowany nauką HTML, CSS i innych technologii frontendowych, możesz zajrzeć do przewodnika Mozilli „Pierwsze kroki w sieci”.

To wszystko, czego potrzebujemy do HTML, więc zapisz plik i wyjdź za pomocą CTRL+X. Teraz możemy przejść do kodu serwera.

Do tego ćwiczenia będziemy pracować na htmlFile.js. Otwórz go w edytorze tekstu:

nano htmlFile.js

Ponieważ musimy odczytać plik, zacznijmy od importu modułu fs:

const http = require("http");
const fs = require('fs').promises;
...

Ten moduł zawiera funkcję readFile(), której użyjemy do załadowania pliku HTML w miejscu. Importujemy wariant oparty na obietnicach, zgodnie z najlepszymi praktykami nowoczesnego JavaScript. Używamy obietnic, ponieważ jest to składniowo bardziej zwięzłe niż wywołania zwrotne (callbacks), których musielibyśmy użyć, gdybyśmy przypisali fs po prostu do require('fs'). Aby dowiedzieć się więcej o najlepszych praktykach programowania asynchronicznego, możesz przeczytać nasz przewodnik Jak pisać kod asynchroniczny w Node.js.

Chcemy, aby nasz plik HTML był odczytywany, gdy użytkownik korzysta z naszego systemu. Zacznijmy od zmodyfikowania requestListener(), aby odczytywał plik:

...
const requestListener = function (req, res) {
    fs.readFile(__dirname + "/index.html")
};
...

Używamy metody fs.readFile() do wczytania pliku. Jej argument to __dirname + "/index.html". Specjalna zmienna __dirname zawiera absolutną ścieżkę miejsca, w którym uruchamiany jest kod Node.js. Następnie dodajemy /index.html, aby móc wczytać wcześniej utworzony plik HTML.

Teraz zwróćmy stronę HTML, gdy zostanie załadowana:

...
const requestListener = function (req, res) {
    fs.readFile(__dirname + "/index.html")
        .then(contents => {
            res.setHeader("Content-Type", "text/html");
            res.writeHead(200);
            res.end(contents);
        })
};
...

Jeśli obietnica fs.readFile() zostanie pomyślnie spełniona, zwróci swoje dane. Używamy metody then(), aby obsłużyć ten przypadek. Parametr contents zawiera dane pliku HTML.

Najpierw ustawiamy nagłówek Content-Type na text/html, aby poinformować klienta, że zwracamy dane HTML. Następnie zapisujemy kod statusu, aby wskazać, że żądanie zakończyło się powodzeniem. Na końcu wysyłamy klientowi stronę HTML, którą załadowaliśmy, z danymi w zmiennej contents.

Metoda fs.readFile() czasami może zawieść, więc powinniśmy obsłużyć tę sytuację, gdy wystąpi błąd. Dodaj to do funkcji requestListener():

...
const requestListener = function (req, res) {
    fs.readFile(__dirname + "/index.html")
        .then(contents => {
            res.setHeader("Content-Type", "text/html");
            res.writeHead(200);
            res.end(contents);
        })
        .catch(err => {
            res.writeHead(500);
            res.end('Internal Server Error');
            return;
        });
};
...

Zapisz plik i wyjdź z nano za pomocą CTRL+X.

Kiedy obietnica napotyka błąd, jest odrzucana. Obsługujemy ten przypadek za pomocą metody catch(). Ustawia ona kod statusu na 500, co oznacza, że wystąpił błąd wewnętrzny, i wysyła przyjazny dla użytkownika komunikat o błędzie. W środowisku produkcyjnym nigdy nie należy ujawniać użytkownikom surowych szczegółów błędu, ponieważ może to prowadzić do wycieku poufnych informacji.

Uruchom nasz serwer za pomocą polecenia node:

node htmlFile.js

W przeglądarce internetowej odwiedź http://localhost:8000. Zobaczysz tę stronę:

Teraz zwróciłeś użytkownikowi stronę HTML z serwera. Możesz zakończyć działanie serwera, używając CTRL+C. Zobaczysz powrót zachęty terminala, gdy to zrobisz.

Pisząc kod tego typu w środowisku produkcyjnym, możesz nie chcieć ładować strony HTML za każdym razem, gdy otrzymasz żądanie HTTP. Chociaż ta strona HTML ma około 800 bajtów, bardziej złożone witryny mogą mieć rozmiar kilku megabajtów. Duże pliki mogą długo się ładować. Jeśli twoja strona oczekuje dużego ruchu, najlepiej może być załadowanie plików HTML przy uruchamianiu i zapisanie ich zawartości. Po ich załadowaniu możesz skonfigurować serwer i sprawić, aby nasłuchiwał żądań pod określonym adresem.

Aby zilustrować tę metodę, zobaczmy, jak możemy przekształcić nasz serwer, aby był bardziej wydajny i skalowalny.

Efektywne serwowanie HTML

Zamiast ładować HTML przy każdym żądaniu, w tym kroku załadujemy go raz na początku. Żądanie zwróci dane, które załadowaliśmy przy uruchomieniu.

W terminalu ponownie otwórz skrypt Node.js w edytorze tekstu:

nano htmlFile.js

Zacznijmy od dodania nowej zmiennej, zanim utworzymy funkcję requestListener():

...
let indexFile;

const requestListener = function (req, res) {
...

Kiedy uruchomimy ten program, ta zmienna będzie przechowywać zawartość pliku HTML.

Teraz dostosujmy funkcję requestListener(). Zamiast wczytywać plik, będzie teraz zwracać zawartość indexFile:

...
const requestListener = function (req, res) {
    res.setHeader("Content-Type", "text/html");
    res.writeHead(200);
    res.end(indexFile);
};
...

Następnie przenosimy logikę odczytu pliku z funkcji requestListener() do uruchamiania naszego serwera. Wprowadź następujące zmiany podczas tworzenia serwera:

...

const server = http.createServer(requestListener);

fs.readFile(__dirname + "/index.html")
    .then(contents => {
        indexFile = contents;
        server.listen(port, host, () => {
            console.log(`Server is running on http://${host}:${port}`);
        });
    })
    .catch(err => {
        console.error(`Could not read index.html file: ${err}`);
        process.exit(1);
    });

Zapisz plik i wyjdź z nano za pomocą CTRL+X.

Kod, który odczytuje plik, jest podobny do tego, co napisaliśmy w naszym pierwszym podejściu. Jednak gdy pomyślnie odczytamy plik, teraz zapisujemy jego zawartość do naszej globalnej zmiennej indexFile. Następnie uruchamiamy serwer za pomocą metody listen(). Kluczowe jest to, że plik jest wczytywany zanim serwer zostanie uruchomiony. W ten sposób funkcja requestListener() będzie miała pewność, że zwróci stronę HTML, ponieważ indexFile nie jest już pustą zmienną.

Nasza obsługa błędów również się zmieniła. Jeśli pliku nie można załadować, przechwytujemy błąd i wyświetlamy go w konsoli. Następnie kończymy działanie programu Node.js za pomocą funkcji exit(), nie uruchamiając serwera. Dzięki temu możemy zobaczyć, dlaczego odczyt pliku się nie powiódł, rozwiązać problem, a następnie ponownie uruchomić serwer.

Stworzyliśmy teraz różne serwery internetowe, które zwracają użytkownikowi różne rodzaje danych. Jak dotąd nie korzystaliśmy z żadnych danych żądania, aby określić, co powinno zostać zwrócone. Będziemy musieli używać danych żądania podczas konfigurowania różnych tras lub ścieżek w serwerze Node.js, więc teraz zobaczmy, jak one współpracują.

Krok 4 — Zarządzanie trasami za pomocą obiektu żądania HTTP

Większość stron internetowych, które odwiedzamy, lub interfejsów API, których używamy, zwykle ma więcej niż jeden punkt końcowy, aby umożliwić dostęp do różnych zasobów. Dobrym przykładem byłby system zarządzania książkami, taki, który mógłby być używany w bibliotece. Musiałby on nie tylko zarządzać danymi książek, ale także danymi autorów w celu ułatwienia katalogowania i wyszukiwania.

Chociaż dane dotyczące książek i autorów są powiązane, są to dwa różne obiekty. W takich przypadkach programiści oprogramowania zwykle kodują każdy obiekt na różnych endpointach, aby wskazać użytkownikowi API, z jakiego rodzaju danymi mają do czynienia.

Stwórzmy nowy serwer dla małej biblioteki, który zwróci dwa różne typy danych. Jeśli użytkownik przejdzie pod adres naszego serwera /books, otrzyma listę książek w formacie JSON. Jeśli przejdzie pod /authors, otrzyma listę informacji o autorach w formacie JSON.

Jak dotąd serwer zwraca taką samą odpowiedź na każde żądanie. Aby obsługiwać wiele zasobów, przechowuj dane w zmiennych i używaj warunkowego routingu. Na przykład:

const books = JSON.stringify([
  { title: "The Alchemist", author: "Paulo Coelho", year: 1988 },
  { title: "The Prophet", author: "Kahlil Gibran", year: 1923 }
]);
const authors = JSON.stringify([
  { name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 },
  { name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 }
]);

const requestListener = function (req, res) {
  res.setHeader("Content-Type", "application/json");
  switch (req.url) {
    case "/books":
      res.writeHead(200);
      res.end(books);
      break;
    case "/authors":
      res.writeHead(200);
      res.end(authors);
      break;
    default:
      res.writeHead(404);
      res.end(JSON.stringify({error:"Resource not found"}));
  }
}

Przetestuj za pomocą curl http://localhost:8000/books lub /authors, aby zobaczyć odpowiednie dane. Każdy inny adres URL zwraca błąd 404.

Problem z prostym dopasowaniem ciągu znaków: req.url jest surowym ciągiem znaków, który zawiera wszystko po domenie — ścieżki, ciągi zapytań i fragmenty. Jeśli ktoś odwiedzi /books?id=123, Twój case "/books" nie zadziała, ponieważ req.url to /books?id=123, a nie /books. Parametry zapytań mogą zepsuć routing: /books?sort=name nie dopasuje się do /books. Zakodowane w URL-u znaki mogą zepsuć routing: /books%20reviews (spacja zakodowana jako %20) nie dopasuje się do /books reviews.

Zamiast tego użyj URL API: API URL automatycznie rozdziela ścieżkę od parametrów zapytania, dekoduje znaki zakodowane w URL i poprawnie obsługuje przypadki brzegowe.

const host = req.headers.host || 'localhost:8000';
const url = new URL(req.url, `http://${host}`);

switch (url.pathname) {
  case '/books':
    // Validate HTTP method - only GET allowed for this endpoint
    if (req.method !== 'GET') {
      res.writeHead(405, { 'Allow': 'GET' });
      return res.end(JSON.stringify({ error: 'Method not allowed' }));
    }
    res.writeHead(200);
    return res.end(books);

  case '/authors':
    if (req.method !== 'GET') {
      res.writeHead(405, { 'Allow': 'GET' });
      return res.end(JSON.stringify({ error: 'Method not allowed' }));
    }
    res.writeHead(200);
    return res.end(authors);

  default:
    res.writeHead(404);
    return res.end(JSON.stringify({ error: 'Resource not found' }));
}

Dlaczego sprawdzać metodę HTTP? Ta sama ścieżka może obsługiwać różne metody HTTP (GET do pobierania, POST do tworzenia). Bez tej kontroli żądanie POST do /books zwróciłoby listę książek zamiast tworzyć nową, co łamie konwencje REST API. Kod statusu 405 Method Not Allowed z nagłówkiem Allow informuje klientów, które metody są dozwolone dla tego endpointu.

Obsługa ciągu zapytania: API URL automatycznie oddziela ścieżkę (/books) od parametrów zapytania (?id=123), dzięki czemu /books?id=123 poprawnie kieruje do punktu końcowego książek, a jednocześnie możesz nadal uzyskać dostęp do url.searchParams.get('id'), jeśli jest to potrzebne.

Przetestuj różne scenariusze:

curl http://localhost:8000/books

curl http://localhost:8000/authors  

curl http://localhost:8000/books?id=123  # Query string doesn't break routing

curl -X POST http://localhost:8000/books  # Returns 405 Method Not Allowed

curl http://localhost:8000/nonexistent  # Returns 404

Krok 5 — Obsługa żądań POST i analizowanie ciał żądań

Jak dotąd obsługiwaliśmy tylko żądania GET. Większość interfejsów API musi przyjmować dane od klientów, co zazwyczaj odbywa się za pomocą żądań POST. Moduł http wymaga ręcznego analizowania treści żądań, które są odbierane jako strumienie.

Krytyczna uwaga: W przeciwieństwie do żądań GET, gdzie dane znajdują się w URL, ciała żądań POST przychodzą jako strumień. Oznacza to, że dane nie docierają wszystkie naraz — przychodzą w kawałkach. Jeśli spróbujesz sparsować JSON zanim wszystkie kawałki dotrą, otrzymasz niekompletne lub źle sformatowane dane. Najpierw musisz zebrać wszystkie kawałki, a dopiero potem sparsować.

Oto co się stanie, jeśli spróbujesz analizować zbyt wcześnie:

// WRONG - Body isn't complete yet
req.on('data', chunk => {
  const data = JSON.parse(chunk); // Will fail - chunk is only part of the body
});

// CORRECT - Collect all chunks first
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
  const data = JSON.parse(body); // Now body is complete
});

Utwórz serwer, który akceptuje żądania POST z danymi JSON. Utwórz nowy plik:

touch post.js

cp routes.js post.js

Otwórz post.js:

nano post.js

Oto parser JSON gotowy do użycia w produkcji, który obsługuje kwestie bezpieczeństwa:

const MAX_BODY = 1 * 1024 * 1024; // 1MB limit prevents memory exhaustion

function parseJSONBody(req, res) {
  return new Promise((resolve, reject) => {
    let body = '';
    let received = 0;
    
    // Validate Content-Type before processing
    const contentType = req.headers['content-type'] || '';
    if (!contentType.startsWith('application/json')) {
      res.writeHead(415, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Unsupported Content-Type' }));
      return reject(new Error('Unsupported Content-Type'));
    }
    
    // Collect data chunks
    req.on('data', chunk => {
      received += chunk.length;
      
      // Prevent memory exhaustion from huge payloads
      if (received > MAX_BODY) {
        res.writeHead(413, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Payload too large' }));
        req.destroy(); // Stop receiving more data
        return reject(new Error('Payload too large'));
      }
      body += chunk.toString(); // Convert Buffer to string
    });
    
    // All chunks received, now parse
    req.on('end', () => {
      try {
        if (!body) {
          res.writeHead(400, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({ error: 'Empty request body' }));
          return reject(new Error('Empty request body'));
        }
        
        const data = JSON.parse(body);
        
        // Validate it's an object (not a string, number, etc.)
        if (typeof data !== 'object' || data === null || Array.isArray(data)) {
          res.writeHead(400, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({ error: 'Invalid JSON object' }));
          return reject(new Error('Invalid JSON object'));
        }
        
        resolve(data);
      } catch (parseError) {
        // JSON.parse() throws for malformed JSON
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Malformed JSON' }));
        console.error('JSON parse error:', parseError.message);
      }
    });
    
    // Handle network errors
    req.on('error', (error) => {
      console.error('Request error:', error.message);
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Request error' }));
    });
  });
}

Dlaczego sprawdzać Content-Type? Jeśli klient wysyła dane formularza z Content-Type: application/x-www-form-urlencoded, próba parsowania ich jako JSON zakończy się niepowodzeniem. Kod stanu 415 Unsupported Media Type informuje klienta, że jego Content-Type nie jest akceptowalny.

Dlaczego ograniczać rozmiar ciała? Bez ograniczenia złośliwy klient mógłby wysłać gigabajty danych, wyczerpując pamięć serwera. Odpowiedź 413 Payload Too Large i req.destroy() natychmiast przerywają połączenie, gdy limit zostanie przekroczony.

Dlaczego weryfikować sparsowany wynik? JSON.parse('"hello"') działa, ale zwraca ciąg znaków, a nie obiekt. Twój kod może oczekiwać, że data.property zadziała, ale ciągi znaków nie mają właściwości. Ta weryfikacja zapewnia, że otrzymasz faktyczny obiekt.

Użyj tej funkcji w swoim obsługiwaczu POST:

const requestListener = async function (req, res) {
  if (req.method === 'POST' && req.url === '/books') {
    const bookData = await parseJSONBody(req, res);
    // Now use bookData safely
    // ...
  }
};

Krok 6 — Serwowanie plików statycznych

Wiele aplikacji internetowych musi udostępniać statyczne zasoby, takie jak obrazy, pliki CSS, pliki JavaScript czy inne zasoby. Chociaż moduł http nie oferuje wbudowanego serwowania plików statycznych, jak express.static w Express, można to zaimplementować przy użyciu modułów fs i path.

Krytyczny problem bezpieczeństwa: Jeśli naiwnie użyjesz path.join(__dirname, 'public', req.url) bez weryfikacji, atakujący może uzyskać dostęp do dowolnego pliku na twoim serwerze, wykorzystując ataki typu directory traversal. Żądanie takie jak GET /../../../etc/passwd mogłoby ujawnić pliki systemowe. Sprawdzenie path.resolve() zapewnia, że rozwiązywana ścieżka nigdy nie wychodzi poza katalog public.

Oto co może pójść źle bez prawidłowej weryfikacji ścieżki:

// VULNERABLE - Allows directory traversal
const filePath = path.join(__dirname, 'public', req.url);
// Request: /../../../etc/passwd
// Resolves to: /etc/passwd (outside public directory!)

// SECURE - Validates path stays within public directory
const safePath = path.resolve(publicDir, '.' + reqPath);
if (!safePath.startsWith(publicDir)) {
  res.writeHead(403);
  res.end('Forbidden');
  return;
}
// Request: /../../../etc/passwd
// safePath: /home/user/app/public/../../../etc/passwd
// Resolved: /etc/passwd
// Check fails because /etc/passwd doesn't start with /home/user/app/public

Utwórz bezpieczny statyczny serwer plików:

touch static.js

Otwórz static.js:

nano static.js

Serwuj pliki statyczne wydajnie i bezpiecznie, przesyłając je strumieniowo za pomocą fs.createReadStream() i bezpiecznego rozwiązywania ścieżek:

const http = require("http");
const fs = require("fs");
const path = require("path");

const host = 'localhost';
const port = 8000;

const mimeTypes = {
  '.html': 'text/html',
  '.css': 'text/css',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.gif': 'image/gif',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
  '.pdf': 'application/pdf'
};

const publicDir = path.resolve(__dirname, 'public');

const requestListener = (req, res) => {
  // Normalize the request path
  let reqPath = req.url === '/' ? '/index.html' : req.url;
  
  // Strip query string - ?id=123 shouldn't affect file path
  reqPath = reqPath.split('?')[0];
  
  // Resolve path and check it stays within publicDir
  // The '.' prefix prevents absolute paths from being resolved incorrectly
  const safePath = path.resolve(publicDir, '.' + reqPath);
  
  // Critical security check: ensure resolved path is within publicDir
  if (!safePath.startsWith(publicDir)) {
    res.writeHead(403, { 'X-Content-Type-Options': 'nosniff' });
    res.end('Forbidden');
    return;
  }
  
  // Check if file exists and is actually a file (not a directory)
  fs.stat(safePath, (err, stat) => {
    if (err || !stat.isFile()) {
      res.writeHead(404, { 'X-Content-Type-Options': 'nosniff' });
      res.end('File not found');
      return;
    }
    
    // Determine MIME type from file extension
    const ext = path.extname(safePath).toLowerCase();
    const contentType = mimeTypes[ext] || 'application/octet-stream';
    
    res.setHeader("Content-Type", contentType);
    res.setHeader("X-Content-Type-Options", "nosniff"); // Prevents MIME sniffing attacks
    res.writeHead(200);
    
    // Stream file instead of loading into memory
    // Critical for large files - won't consume all memory
    const stream = fs.createReadStream(safePath);
    stream.pipe(res);
    
    stream.on('error', () => {
      // File might be deleted between stat() and createReadStream()
      res.writeHead(500, { 'X-Content-Type-Options': 'nosniff' });
      res.end('Server error');
    });
  });
};

const server = http.createServer(requestListener);
server.listen(port, host, () => {
  console.log(`Server is running on http://${host}:${port}`);
  console.log(`Serving static files from: ${publicDir}`);
});

Utwórz katalog public i dodaj plik testowy:

mkdir public

echo "<h1>Static File Served!</h1>" > public/index.html

Uruchom serwer:

node static.js

Odwiedź http://localhost:8000 w swojej przeglądarce, aby zobaczyć serwowany plik statyczny.

Dlaczego używać strumieniowania zamiast fs.readFile()? W przypadku pliku wideo o rozmiarze 100 MB, fs.readFile() ładuje cały plik do pamięci zanim go wyśle. Dzięki fs.createReadStream() dane przepływają bezpośrednio z dysku do sieci w małych porcjach, zużywając minimalną ilość pamięci niezależnie od rozmiaru pliku.

Dlaczego sprawdzać stat.isFile()? Jeśli ktoś poprosi o ścieżkę katalogu, np. /images/, fs.stat() powiedzie się, ale isFile() zwróci false. Bez tego sprawdzenia spróbowałbyś przesłać katalog, co zakończy się niepowodzeniem. To sprawdzenie zapewnia, że serwujesz tylko faktyczne pliki.

Przetestuj ochronę przed przechodzeniem po ścieżkach:

curl http://localhost:8000/../../../etc/passwd

# Should return 403 Forbidden, not the system file

Krok 7 — Obsługa błędów i logowanie

Solidne obsługiwanie błędów jest kluczowe dla serwerów produkcyjnych. Moduł http nie zapewnia automatycznej obsługi błędów, więc musisz zaimplementować ją samodzielnie.

Właściwe obsługiwanie błędów obejmuje wychwytywanie błędów asynchronicznych, odpowiednie ich logowanie, wysyłanie przyjaznych dla użytkownika komunikatów o błędach oraz zapobieganie awariom serwera spowodowanym nieobsłużonymi błędami. Ulepszmy nasz serwer, wprowadzając kompleksową obsługę błędów i logowanie:

const http = require("http");
const fs = require('fs').promises;

const host = 'localhost';
const port = 8000;

// Simple logging utility
const log = {
    info: (msg) => console.log(`[INFO] ${new Date().toISOString()} - ${msg}`),
    error: (msg, err) => console.error(`[ERROR] ${new Date().toISOString()} - ${msg}`, err),
    warn: (msg) => console.warn(`[WARN] ${new Date().toISOString()} - ${msg}`)
};

const requestListener = async function (req, res) {
    try {
        log.info(`${req.method} ${req.url} - ${req.headers['user-agent'] || 'Unknown'}`);
        if (req.url === '/') {
            try {
                const data = await fs.readFile(__dirname + '/index.html');
                res.setHeader("Content-Type", "text/html");
                res.writeHead(200);
                res.end(data);
            } catch (error) {
                log.error('Failed to read index.html', error);
                res.writeHead(500);
                res.end('<h1>Internal Server Error</h1><p>Unable to load page.</p>');
            }
        } else if (req.url === '/error') {
            throw new Error('Simulated error for testing');
        } else {
            res.writeHead(404);
            res.end('<h1>Not Found</h1><p>The requested resource was not found.</p>');
        }
    } catch (error) {
        log.error('Unhandled error in request handler', error);
        const errorMessage = process.env.NODE_ENV !== 'production'
            ? error.message
            : 'An unexpected error occurred';
        res.writeHead(500);
        res.end(`<h1>Internal Server Error</h1><p>${errorMessage}</p>`);
    }
};

const server = http.createServer(requestListener);

// Handle server-level errors
server.on('error', (error) => {
    if (error.code === 'EADDRINUSE') {
        log.error(`Port ${port} is already in use`);
    } else {
        log.error('Server error', error);
    }
    process.exit(1);
});

// Handle client connection errors
server.on('clientError', (err, socket) => {
    log.warn('Client error: ' + err.message);
    socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.listen(port, host, () => {
    log.info(`Server is running on http://${host}:${port}`);
});

Łagodne zamknięcie (SIGTERM/SIGINT)

Podczas uruchamiania pod menedżerami procesów takimi jak PM2 czy Docker, obsługuj sygnały zakończenia, aby zamknąć serwer w sposób łagodny:

const connections = new Set();

server.on('connection', socket => {
  connections.add(socket);
  socket.on('close', () => connections.delete(socket));
});

const shutdown = (signal) => {
  log.warn(`${signal} received: shutting down gracefully...`);
  server.close(() => {
    log.info('Server closed.');
    process.exit(0);
  });
  setTimeout(() => {
    log.warn('Forcing socket close...');
    connections.forEach(s => s.destroy());
    process.exit(1);
  }, 10000).unref();
};

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

Pokazane kluczowe praktyki obsługi błędów:

  1. Bloki try-catch wokół operacji asynchronicznych
  2. Obsługa błędów na poziomie serwera dla problemów z połączeniem i portem
  3. Obsługa błędów klienta dla niepoprawnych żądań
  4. Rejestrowanie z sygnaturami czasowymi i poziomami ważności
  5. Komunikaty o błędach zależne od środowiska (nie ujawniaj śladów stosu w produkcji)
  6. Eleganckie odpowiedzi na błędy z odpowiednimi kodami statusu HTTP

Najlepsza praktyka: W środowisku produkcyjnym warto rozważyć korzystanie z bibliotek do logowania strukturalnego, takich jak Winston czy Pino, oraz wysyłanie krytycznych błędów do usług śledzenia błędów, takich jak Sentry czy Rollbar. Ten przykład używa prostego logowania do konsoli w celach edukacyjnych.

Krok 8 — Używanie składni modułów ES (ESM)

Nowoczesny Node.js obsługuje moduły ES (ESM) używając składni import/export zamiast CommonJS require/module.exports. Aby korzystać z ESM, należy albo nadać plikowi rozszerzenie .mjs, albo ustawić "type": "module" w swoim package.json.

Składnia ESM używa import zamiast require oraz export zamiast module.exports. Oto ten sam podstawowy serwer z użyciem składni modułów ES:

Utwórz server.mjs:

touch server.mjs

nano server.mjs
import http from 'http';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

// ES Modules don't have __dirname, so we recreate it
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const host = 'localhost';
const port = 8000;

const requestListener = async (req, res) => {
    try {
        if (req.url === '/') {
            const data = await fs.readFile(path.join(__dirname, 'index.html'));
            res.setHeader("Content-Type", "text/html");
            res.writeHead(200);
            res.end(data);
        } else {
            res.writeHead(404);
            res.end('Not found');
        }
    } catch (error) {
        console.error('Error:', error);
        res.writeHead(500);
        res.end('Server error');
    }
};

const server = http.createServer(requestListener);

server.listen(port, host, () => {
    console.log(`Server is running on http://${host}:${port}`);
});

Uruchom to:

node server.mjs

Kluczowe różnice: W ESM __dirname nie jest dostępne — musisz je wyprowadzić z import.meta.url. Dodatkowo, ścieżki plików w operacjach fs mogą wymagać jawnego rozstrzygania. Poza tym API modułu HTTP pozostaje identyczne.

Alternatywnie, utwórz package.json z "type": "module":

{
  "type": "module"
}

Następnie użyj plików .js ze składnią ESM:

import http from 'http';

const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('ESM Server');
});

server.listen(8000);

Kwestie produkcyjne

Przy przechodzeniu z fazy rozwoju do produkcji należy wziąć pod uwagę kilka istotnych kwestii:

Obsługa HTTPS

Moduł http obsługuje wyłącznie HTTP. W przypadku HTTPS użyj podobnie modułu https:

const https = require('https');
const fs = require('fs');

const options = {
    key: fs.readFileSync('path/to/private-key.pem'),
    cert: fs.readFileSync('path/to/certificate.pem')
};

const server = https.createServer(options, requestListener);

Bezpieczeństwo: Nigdy nie umieszczaj prywatnych kluczy ani certyfikatów w systemie kontroli wersji. W środowisku produkcyjnym używaj zmiennych środowiskowych lub bezpiecznych usług zarządzania sekretami.

Konfiguracja serwera proxy odwrotnego

W środowisku produkcyjnym serwery Node.js zazwyczaj działają za pośrednictwem serwerów proxy, takich jak Nginx czy Apache. Serwer proxy obsługuje zakończenie SSL, pliki statyczne oraz równoważenie obciążenia. Twój serwer Node.js powinien:

  • Nasłuchuj tylko na localhost lub 127.0.0.1
  • Odczytuj nagłówki X-Forwarded-For w celu uzyskania adresów IP klientów
  • Ufaj nagłówkom proxy tylko od swojego serwera proxy

Zobacz nasz przewodnik Jak Skonfigurować Nginx jako Reverse Proxy na Ubuntu, aby poznać szczegóły konfiguracji.

Zarządzanie procesami

Do produkcji używaj menedżerów procesów, takich jak PM2, aby obsługiwać:

  • Automatyczne ponowne uruchamianie po awarii
  • Wdrożenia bez przestojów
  • Zarządzanie dziennikami
  • Tryb klastrowy do wykorzystania wielu rdzeni CPU

Dowiedz się więcej w naszym przewodniku Jak skonfigurować aplikację Node.js do produkcji na Ubuntu.

Optymalizacja wydajności

  • Pamięć podręczna plików: Ładuj pliki statyczne podczas uruchamiania (jak pokazano w Kroku 3), zamiast odczytywać je z dysku przy każdym żądaniu. Znacznie zmniejsza to obciążenie operacji wejścia/wyjścia.

  • Strumieniuj duże pliki: Dla plików większych niż 1MB użyj fs.createReadStream() i przekieruj do odpowiedzi zamiast wczytywać cały plik do pamięci.

  • Grupowanie połączeń: W przypadku połączeń z bazą danych używaj grupowania połączeń, aby uniknąć kosztów związanych z tworzeniem nowych połączeń dla każdego żądania.

  • Kompresja: Użyj modułu zlib do kompresji gzip odpowiedzi tekstowych, aby zmniejszyć zużycie przepustowości:

    const zlib = require('zlib');
    const gzip = zlib.createGzip();
    res.setHeader('Content-Encoding', 'gzip');
    gzip.pipe(res);
    gzip.write(data);
    gzip.end();
  • Tryb klastra: Użyj modułu Node.js cluster, aby uruchomić procesy robocze równe liczbie rdzeni CPU, maksymalizując przepustowość dla operacji wymagających dużej mocy obliczeniowej.

  • Przekroczenie czasu żądania: Ustaw rozsądne wartości limitu czasu, aby zapobiec zużywaniu zasobów przez wolnych klientów:

    server.setTimeout(30000); // 30 seconds
  • Monitorowanie pamięci: Śledź użycie pamięci, aby wcześnie wykrywać wycieki. W środowisku produkcyjnym monitoruj rozmiar sterty i wdrażaj łagodne ponowne uruchamianie, jeśli pamięć rośnie w sposób niekontrolowany.

Najlepsze praktyki dotyczące buforowania i kompresji

Aby jeszcze bardziej poprawić wydajność i skrócić czas reakcji, stosuj buforowanie i kompresję tam, gdzie jest to odpowiednie.

  • Włącz pamięć podręczną dla plików statycznych:
    Dodaj nagłówki Cache-Control i ETag do odpowiedzi dla plików statycznych. Na przykład:

    const stat = fs.statSync(safePath);
    res.setHeader('Cache-Control', 'public, max-age=86400'); // cache for 1 day
    res.setHeader('ETag', `"${stat.size}-${stat.mtime.getTime()}"`);

    Przeglądarki i sieci CDN będą ponownie wykorzystywać zapisane w pamięci podręcznej kopie, dopóki nie wygaśnie ETag lub czas przechowywania w pamięci podręcznej.

  • Użyj kompresji gzip lub Brotli:
    Odpowiedzi tekstowe, takie jak HTML, CSS, JS i JSON, kompresują się bardzo dobrze. Możesz zastosować kompresję za pomocą modułu zlib w Node:

    const zlib = require('zlib');
    const stream = fs.createReadStream(safePath);
    res.setHeader('Content-Encoding', 'gzip');
    stream.pipe(zlib.createGzip()).pipe(res);

    To zmniejsza zużycie przepustowości i przyspiesza dostarczanie dla klientów z wolniejszymi połączeniami.

  • Unikaj nadmiernego buforowania dynamicznych punktów końcowych:
    Dla interfejsów API lub często aktualizowanych danych ustaw krótsze czasy przechowywania w pamięci podręcznej (np. max-age=60) lub w razie potrzeby użyj no-store.

Te optymalizacje uzupełniają przesyłanie statycznych plików i łagodne zamykanie serwera, pomagając Twojemu serwerowi Node.js osiągać wysoką wydajność przy zachowaniu bezpieczeństwa i przewidywalności.

Używaj modułu http Node.js do minimalnych API, niestandardowego routingu lub nauki wewnętrznej struktury HTTP.
Używaj Express/Fastify, gdy potrzebujesz middleware, szablonów lub szybkiego prototypowania.

Rozwiązywanie typowych problemów

Port już w użyciu (EADDRINUSE)

Błąd: Błąd: listen EADDRINUSE: adres już używany :::8000

Rozwiązanie:

  • Znajdź proces korzystający z portu: lsof -i :8000 (macOS/Linux) lub netstat -ano | findstr :8000 (Windows)
  • Zabij proces lub użyj innego portu
  • Dodaj obsługę błędów (jak pokazano w kroku 7), aby przechwycić to w elegancki sposób

Ciało żądania nie zostało odebrane

Problem: Treść żądania POST jest pusta lub niezdefiniowana

Rozwiązanie: Ciała żądań są strumieniami – musisz zbierać fragmenty. Zawsze stosuj wzorzec pokazany w Kroku 5:

let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
    // Now parse body
});

Błędy CORS w przeglądarce

Problem: Przeglądarka blokuje żądania z powodu polityki CORS

Rozwiązanie: Dodaj nagłówki CORS w swojej odpowiedzi:

res.setHeader("Access-Control-Allow-Origin", "*"); // Or specific domain
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");

Bezpieczeństwo: Użycie "*" w Access-Control-Allow-Origin pozwala na dostęp z dowolnego źródła. W środowisku produkcyjnym określ wyraźnie domenę swojego frontendu.

Pliki statyczne zwracają 404

Problem: Pliki statyczne nie są znajdowane, mimo że istnieją

Rozwiązanie:

  • Sprawdź rozwiązywanie ścieżki pliku
  • Sprawdź, czy __dirname wskazuje na poprawny katalog
  • Upewnij się, że uprawnienia do pliku pozwalają na odczyt
  • Sprawdź, czy oczyszczanie ścieżki nie usuwa poprawnych znaków

Najczęściej zadawane pytania

Czym jest moduł HTTP w Node.js?

Moduł http to wbudowany moduł Node.js do tworzenia serwerów i klientów HTTP. Udostępnia niskopoziomowe API do obsługi żądań i odpowiedzi HTTP bez konieczności używania zewnętrznych zależności. Jest częścią rdzenia Node.js, więc jest dostępny w każdej instalacji Node.js.

Jak stworzyć prosty serwer internetowy w Node.js?

Utwórz serwer za pomocą http.createServer() z funkcją nasłuchującą żądania:

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello World');
});

server.listen(8000);

To tworzy serwer nasłuchujący na porcie 8000, który odpowiada na wszystkie żądania słowami „Hello World”.

Jaka jest różnica między modułem HTTP a Express.js?

Moduł http jest podstawową funkcjonalnością Node.js, zapewniającą niskopoziomową obsługę HTTP. Express.js to framework zbudowany na bazie http, który dodaje routowanie, middleware, szablony i inne udogodnienia. Moduł http daje pełną kontrolę, ale wymaga więcej ręcznej konfiguracji, podczas gdy Express.js zapewnia abstrakcje kosztem dodatkowych zależności.

Używaj modułu http kiedy:

  • Tworzenie lekkich interfejsów API lub mikrousług
  • Nauka podstaw HTTP
  • Chcesz zerowych zależności
  • Pełna kontrola nad obsługą żądań/odpowiedzi

Używaj Express.js, gdy:

  • Tworzenie złożonych aplikacji z wieloma trasami
  • Potrzebujesz ekosystemu middleware
  • Szybkie prototypowanie jest priorytetem
  • Chcesz wzorców standardowych w społeczności

Aby uzyskać kompleksowe wprowadzenie do Express.js i dowiedzieć się, jak upraszcza rozwój stron internetowych, zobacz nasz przewodnik po podstawach Node.js Express.

Czy mogę stworzyć REST API przy użyciu Node.js bez Express?

Tak, absolutnie. Moduł http wystarczy do budowania REST API. Będziesz musiał ręcznie obsłużyć:

  • Analiza trasy (z użyciem req.url i req.method)
  • Parsowanie treści żądania (zbieranie fragmentów strumienia)
  • Formatowanie odpowiedzi
  • Obsługa błędów

Dla prostych interfejsów API często jest to lepsze niż dodawanie zależności od frameworków. Zobacz kroki 4 i 5 tego samouczka, aby znaleźć przykłady routingu i obsługi żądań POST.

Jak obsługiwać różne trasy za pomocą modułu HTTP w Node.js?

Sparś req.url i użyj logiki warunkowej lub instrukcji switch:

const requestListener = (req, res) => {
    switch (req.url) {
        case '/':
            res.writeHead(200);
            res.end('Home page');
            break;
        case '/about':
            res.writeHead(200);
            res.end('About page');
            break;
        default:
            res.writeHead(404);
            res.end('Not found');
    }
};

Dla bardziej zaawansowanego routingu możesz analizować ścieżki URL i ciągi zapytań za pomocą modułu url oraz obsługiwać różne metody HTTP.

Jak wysyłać odpowiedzi HTML lub JSON przy użyciu modułu HTTP?

Ustaw nagłówek Content-Type odpowiednio i wyślij dane:

HTML:

res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end('<h1>Hello</h1>');

JSON:

res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify({ message: 'Hello' }));

Nagłówek Content-Type informuje klienta, jak interpretować dane odpowiedzi.

Jakiego portu powinienem użyć dla lokalnego serwera Node.js?

Typowe porty używane w trakcie tworzenia to:

  • 8000 lub 8080: Ogólny rozwój HTTP
  • 3000: Popularny wśród frameworków takich jak Express
  • 5000: Również powszechne

Każdy port powyżej 1024 działa dla użytkowników nieuprzywilejowanych. Porty poniżej 1024 wymagają uprawnień administratora. Sprawdź dostępność za pomocą lsof -i :PORT (macOS/Linux) przed użyciem.

Jak mogę obsługiwać błędy 404 w serwerze HTTP Node.js?

Dodaj przypadek domyślny w swojej logice routingu:

const requestListener = (req, res) => {
    if (req.url === '/') {
        res.writeHead(200);
        res.end('Home');
    } else {
        res.writeHead(404);
        res.end('Not Found');
    }
};

Zawsze ustawiaj kod statusu na 404 dla nieistniejących tras, aby przestrzegać standardów HTTP.

Jak serwować pliki statyczne bez Express?

Użyj modułu fs do odczytu plików oraz modułu path do bezpiecznego ustalania ścieżek:

const fs = require('fs').promises;
const path = require('path');

const requestListener = async (req, res) => {
    const filePath = path.join(__dirname, 'public', req.url);
    try {
        const data = await fs.readFile(filePath);
        res.writeHead(200);
        res.end(data);
    } catch (error) {
        res.writeHead(404);
        res.end('File not found');
    }
};

Zawsze weryfikuj ścieżki, aby zapobiec atakom typu traversal katalogów (zobacz Krok 6, aby uzyskać bezpieczną implementację).

Czy mogę używać modułów ES (import) z modułem HTTP w Node.js?

Tak, Node.js obsługuje moduły ES. Użyj jednego z poniższych:

  1. Plik z rozszerzeniem .mjs:
import http from 'http';
// ... rest of code
  1. Ustaw "type": "module" w package.json, a następnie używaj plików .js:
{
  "type": "module"
}

Zauważ, że moduły ES nie mają __dirname — należy je wyprowadzić z import.meta.url, jak pokazano w Kroku 8.

Jak analizować treści żądań POST za pomocą modułu HTTP?

Ciała żądań POST przychodzą jako strumienie. Zbieraj fragmenty, a następnie analizuj:

const parseBody = (req) => {
    return new Promise((resolve, reject) => {
        let body = '';
        req.on('data', chunk => body += chunk.toString());
        req.on('end', () => {
            try {
                resolve(JSON.parse(body)); // For JSON
            } catch (error) {
                reject(error);
            }
        });
        req.on('error', reject);
    });
};

Ten wzorzec działa dla JSON, danych formularza lub dowolnego typu treści opartej na tekście.

Czy moduł HTTP nadaje się do użytku w produkcji?

Tak, moduł http nadaje się do użycia w produkcji i jest wykorzystywany w wielu aplikacjach Node.js. Jednak warto wziąć pod uwagę:

  • Zarządzanie procesami: Używaj PM2 lub podobnego narzędzia do automatycznego restartu
  • Reverse proxy: Działa za Nginx/Apache w celu SSL i równoważenia obciążenia
  • Obsługa błędów: Wdrożenie kompleksowej obsługi błędów i logowania
  • Bezpieczeństwo: Weryfikuj dane wejściowe, oczyszczaj ścieżki, ustalaj limity rozmiaru żądań
  • Wydajność: Buforuj pliki statyczne, używaj klastrowania do operacji obciążających procesor

Wiele aplikacji produkcyjnych używa bezpośrednio modułu http, zwłaszcza mikroserwisy i lekkie API. Aby uzyskać kompleksowe wskazówki dotyczące wdrażania produkcyjnego, zobacz Jak Skonfigurować Aplikację Node.js do Produkcji na Ubuntu 22.04.

Wniosek

W tym samouczku zbudowałeś serwery HTTP Node.js gotowe do produkcji od podstaw. Zacząłeś od podstawowej odpowiedzi tekstowej, a następnie przeszedłeś do obsługi wielu typów treści (JSON, CSV i HTML). Zaimplementowałeś routing do obsługi różnych adresów URL, parsowałeś ciała żądań POST, bezpiecznie serwowałeś pliki statyczne oraz dodałeś kompleksową obsługę błędów i logowanie. Poznałeś również zarówno składnię CommonJS, jak i ES Module, a także rozważania związane z wdrożeniem produkcyjnym.

Teraz jesteś wyposażony w umiejętność tworzenia serwerów internetowych, które obsługują różnorodne wymagania bez korzystania z frameworków. Dzięki tej wiedzy możesz:

  • Twórz lekkie API REST dla mikroserwisów
  • Obsługuj wielostronicowe witryny z właściwym routowaniem
  • Obsługuj przesyłanie i pobieranie plików
  • Wdrażaj niestandardową logikę middleware
  • Podejmuj świadome decyzje, kiedy frameworki wnoszą wartość, a kiedy niepotrzebnie komplikują

Następne kroki:

  • Zanurz się głębiej w podstawy Node.js: Wróć do serii Jak programować w Node.js, aby uzyskać kompleksowe omówienie
  • Przeglądaj oficjalną dokumentację: Dokumentacja modułu HTTP w Node.js zawiera szczegółowe odniesienia do API oraz zaawansowane przypadki użycia
  • Kiedy potrzebujesz funkcji frameworka: Rozważ podstawy Express.js do szybkiego tworzenia aplikacji z obsługą middleware i szablonów

Zrozumienie tych podstaw HTTP czyni cię bardziej kompetentnym deweloperem Node.js. Niezależnie od tego, czy zdecydujesz się korzystać z frameworków, czy pracować bezpośrednio z modułem http, teraz rozumiesz podstawowe mechanizmy, które napędzają wszystkie aplikacje webowe Node.js. Ta wiedza pomaga w debugowaniu problemów, optymalizacji wydajności i projektowaniu systemów, które skutecznie się skalują.

Dziękujemy za naukę z społecznością Linux-Console.net.