Strona główna / Artykuły

Przygotowanie do rozmów rekrutacyjnych z Node.js i Express: 70 pytań z dokładnymi odpowiedziami

Strukturalny przegląd 70 pytań z rozmów rekrutacyjnych dotyczących Node.js i Express, obejmujący tematy od pętli zdarzeń i strumieni po middleware, bezpieczeństwo oraz skalowanie, wraz z korektami często popełnianych błędów.

Wydawca: REACTAPP.TOP21 min czytania4861 słów

Źródła: Node.js & Express.js Interview Guide

Rozmowy kwalifikacyjne dotyczące stanowisk w Node.js zazwyczaj krążą wokół tych samych tematów: jak silnik jednowątkowy obsługuje tysiące połączeń, co tak naprawdę robi pętla zdarzeń oraz jak Express przekształca surowy serwer HTTP w aplikację. Wypowiedzi zapamiętane na pamięć zawodzą przy dodatkowych pytaniach, dlatego ta przeglądka grupuje 70 najczęściej zadawanych pytań według tematów i podaje do każdej odpowiedzi wystarczająco dużo uzasadnienia, aby można ją było obronić. Gdzie popularna odpowiedź jest przestarzała lub nieprecyzyjna, zaznaczono poprawkę, ponieważ właśnie dostrzeganie tych szczegółów często odróżnia silnego kandydata od tego, który tylko się przygotował.

Zasady Node.js

Q1. Co to jest Node.js?

Jest to środowisko wykonawcze, które uruchamia JavaScript poza przeglądarką. Zawiera silnik V8 firmy Google oraz dodatkowe API do obsługi plików, sieci i procesów. Jego model opiera się na zdarzeniach z nieblokującym wejściem/wyjściem, co sprawia, że jest lekki i doskonale nadaje się do usług wymagających dużej ilości danych oraz działających w czasie rzeczywistym.

Q2. Czy Node.js jest jednowątkowy?

Twój JavaScript działa na jednym głównym wątku, który steruje pętlą zdarzeń. W tle libuv utrzymuje zbiór wątków do obsługi zadań, które nie mają odpowiedniej asynchronicznej interfejsu systemowego, takich jak wywołania systemu plików, dns.lookup, niektóre funkcje crypto oraz zlib. Zatem dokładna odpowiedź brzmi: jednowątkowy dla twojego kodu, wielowątkowy na poziomie systemu.

Q3. Dlaczego Node.js jest szybki?

  • V8 kompiluje JavaScript do kodu maszynowego zamiast go interpretować.
  • Wejście i wyjście są asynchroniczne i nieblokujące.
  • Pętla zdarzeń zapewnia równoczesność bez potrzeby utrzymywania osobnego wątku na każde żądanie.
  • Ten model sprawdza się doskonale przy zadaniami ograniczonych przepustowością sieci lub dysku; słabo radzi sobie z zadaniami wymagającymi dużej mocy obliczeniowej CPU.
  • Q4. Kod synchroniczny versus asynchroniczny

    Operacja synchroniczna kończy się przed uruchomieniem następnej linii kodu, blokując wszystko, co następuje po niej. Operacja asynchroniczna, taka jak odczyt pliku lub zapytanie do bazy danych, jest rozpoczynana i przekazywana dalej; jej wykonywanie trwa dalej, a wynik jest dostarczany później za pomocą funkcji zwrotnej lub obietnicy. W poniższej wersji synchronicznej log czeka na zakończenie całego odczytu (i wyświetla Buffer, ponieważ nie podano żadnego kodowania):

    // Synchronous
    const data = fs.readFileSync('file.txt');
    console.log(data);
    

    W wersji asynchronicznej ostatnia linia logu jest wyświetlana przed treścią pliku, ponieważ odczyt kończy się w późniejszym momencie podczas kolejnego cyklu pętli zdarzeń:

    // Asynchronous
    fs.readFile('file.txt', (err, data) => {
      console.log(data);
    });
    console.log("This runs first!");
    

    Q5. Co to jest npm?

    npm to menedżer pakietów dostępny wraz z Node.js. Używa się go do instalowania, publikowania i zarządzania zależnościami, a także do ich rejestrowania wraz ze skryptami w pliku package.json.

    Q6. package.json versus package-lock.json

    package.json opisuje projekt: jego nazwę, wersję, punkt wejścia, skrypty oraz zakresy zależności, które są akceptowane. package-lock.json określa dokładną wersję każdego zainstalowanego pakietu, włączając te transywne, tak aby na każdym komputerze i podczas wykonywania testów CI instalowano identyczną strukturę. Zapisz oba pliki.

    Q7. Które moduły podstawowe powinieneś znać?

    Powszechnie wymieniane wbudowane moduły to fs, path, http i https, os, events, stream, buffer, crypto, util, url, querystring (obecnie uważany za przestarzały na rzecz URLSearchParams), child_process, cluster oraz zlib.

    Q8. require() versus import

    Różnice pomiędzy CommonJS a modułami ES pojawiają się ciągle:

    • require ładuje dane synchronicznie; moduły ES są ładowane asynchronicznie i oferują również dynamiczne import(), które zwraca obietnicę.
    • CommonJS jest standardem dla plików .js; moduły ES wymagają pola "type": "module" w pliku package.json lub rozszerzenia .mjs.
  • CommonJS eksportuje za pomocą module.exports; moduły ES używają export oraz export default.
  • require można wywołać w dowolnym miejscu, włącznie z wnętrzem warunków; statyczne deklaracje import są przenoszone na górę i muszą znajdować się na najwyższym poziomie.
  • Q9. module.exports versus exports

    exports początkowo odnosi się do tego samego obiektu co module.exports. Dodawanie do niego właściwości działa, ale przypisanie nowej wartości (exports = {...}) jedynie ponownie powiązuje zmienną lokalną, a moduł nadal eksportuje oryginalny obiekt. Aby zastąpić cały eksport, należy przypisać wartość do module.exports.

    Q10. Co to jest REPL?

    REPL to skrót od Read, Eval, Print, Loop. Uruchomienie node bez argumentów otwiera interaktywną shell, która ewaluje każdą wpisaną przez użytkownika wyrażenie i wyświetla wynik, co jest przydatne do szybkich eksperymentów.

    Pętla zdarzeń i wzorce asynchroniczne

    Q11. Co to jest pętla zdarzeń?

    To mechanizm umożliwiający pojedynczemu wątkowi JavaScript wykonywanie operacji wejścia/wyjścia w trybie nieblokującym. Gdy stos wywołań jest pusty, pętla bierze następny gotowy callback z kolejek i go uruchamia. Szerszy opis sposobu, w jaki pętla ta współpracuje z libuv, znajduje się w naszym artykule o libuv, pętli zdarzeń i puli wątków.

    Q12. Jakie są fazy pętli zdarzeń?

    1. Czasy wyznaczone: callbacki z setTimeout i setInterval, których czas już nadszedł.
  • Czekające na wykonanie callbacki: niektóre callbacki związane z operacjami systemowymi odłożone z poprzedniej iteracji.
  • Czas bezczynności i przygotowania: wewnętrzne zadania konserwacyjne.
  • Poll: zbiera nowo zakończone wydarzenia wejścia/wyjścia i uruchamia ich callbacki.
  • Kontrola: callbacki z użyciem setImmediate().
  • Callbacki zamknięcia: obsługi takie jak socket.on('close').
  • Kolejki mikrozadań (process.nextTick najpierw, a potem reakcje na obietnice) są opróżniane po każdym pojedynczym callbacku, a nie tylko pomiędzy fazami. Takie zachowanie występuje od wersji Node 11 i jest często zadawane pytanie.

    Q13. nextTick, obietnice, setTimeout i setImmediate

    • Callbacki z process.nextTick() są uruchamiane zaraz po zakończeniu bieżącej operacji, przed mikrozadaniami opartymi na obietnicach.
  • Reakcje typu Promise, takie jak .then, są wykonywane po tym, jak kolejka nextTick zostanie opróżniona, ale jeszcze przed kontynuacją pętli.
  • setTimeout(fn, 0) jest uruchamiany w fazie timerów po upływie minimalnego opóźnienia.
  • setImmediate() jest uruchamiany w fazie sprawdzania, tuż po fazie przeglądania tej samej iteracji.
  • To skrypt wykorzystuje wszystkie cztery z nich:

    console.log('start');
    setTimeout(() => console.log('setTimeout'), 0);
    setImmediate(() => console.log('setImmediate'));
    process.nextTick(() => console.log('nextTick'));
    Promise.resolve().then(() => console.log('promise'));
    console.log('end');
    

    Oczekiwany wynik pokazuje najpierw logi synchroniczne, a następnie mikrozadania. Dwa makrozadania mogą pojawić się w dowolnej kolejności po zaplanowaniu z modułu głównego, ponieważ zależy to od tego, czy upłynęła już granica czasowa timera w momencie rozpoczęcia pętli; wewnątrz funkcji zwrotnej pliku lub socketu setImmediate zawsze ma pierwszeństwo:

    // Output:
    // start
    // end
    // nextTick
    // promise
    // setTimeout (or setImmediate — order can vary if not inside I/O)
    // setImmediate (or setTimeout)
    

    Q14. Funkcje zwrotne i piekło funkcji zwrotnych

    Callback to funkcja, którą przekazujemy operacji, aby mogła zostać wywołana po jej zakończeniu. Gdy każdy krok zależy od poprzedniego, callbacki układają się coraz głębiej, tworząc strukturę przypominającą piramidę znaną jako „callback hell”, co utrudnia czytanie kodu oraz obsługę błędów w:

    getUser(id, (user) => {
      getPosts(user, (posts) => {
        getComments(posts, (comments) => {
          // deeply nested - hard to manage
        });
      });
    });
    

    Promises oraz async/await uproszczają tę strukturę.

    Q15. Co to jest Promise?

    Promise to obiekt reprezentujący przyszły wynik operacji asynchronicznej. Może być w stanie oczekiwania, spełniony z określoną wartością lub odrzucony z powodem; po ustaleniu się jego stanu nie zmienia się już więcej. Do obsługi zdarzeń używamy metod .then i .catch:

    const promise = new Promise((resolve, reject) => {
      if (success) resolve(data);
      else reject(error);
    });
    promise.then(data => console.log(data)).catch(err => console.error(err));
    

    Q16. Co to jest async/await?

    Jest to składnia oparta na obietnicach, która umożliwia asynchronicznemu kodowi przetwarzanie danych od góry do dołu. Funkcja async zawsze zwraca obietnicę, a await wstrzymuje jej wykonywanie do chwili, aż oczekiwana obietnica zostanie zrealizowana lub odrzucona, przy czym odrzucenia pojawiają się jako wyjątki, które można przechwycić:

    async function getUser() {
      try {
        const res = await fetch('/api/user');
        const data = await res.json();
        return data;
      } catch (err) {
        console.error(err);
      }
    }
    

    Należy zauważyć, że ten przykład loguje błąd i domyślnie zwraca undefined, więc wywołujący nie mogą odróżnić niepowodzenia od pustego wyniku. Często lepszym rozwiązaniem jest ponowne rzucenie wyjątku.

    Q17. Kombinatory obietnic

    • Promise.all() zwraca wartość, gdy wszystkie wejścia zostaną zrealizowane, a odrzuca wartość natychmiast po tym, jak jakiekolwiek wejście zostanie odrzucone.
    • Promise.race() zwraca wartość pierwszego wejścia, które zostanie zrealizowane lub odrzucone.
    • Promise.allSettled() czeka na wszystkie wejścia i raportuje każdy wynik.
  • Promise.any() realizuje się przy pierwszym zrealizowanym elemencie wejściowym i odrzuca (z błędem AggregateError) tylko wtedy, gdy wszystkie zostaną odrzucone.
  • Q18. Kod blokujący versus nieblokujący

    Kod blokujący zatrzymuje wszystko inne, dopóki nie zostanie zakończony, tak jak robi to fs.readFileSync. Kod nieblokujący rozpoczyna pracę i pozwala programowi kontynuować działanie, tak jak robi to fs.readFile. Na serwerze jedna wywołanie blokujące zatrzymuje wszystkie trwające żądania.

    Strumienie, bufory i zdarzenia

    Q19. Co to są strumienie?

    Strumienie przetwarzają dane stopniowo, po fragmentach, zamiast przechowywać je wszystkie w pamięci. Są odpowiednim narzędziem do dużych plików, mediów i ruchu sieciowego. Istnieją cztery rodzaje:

    • Dostępne do odczytu, takie jak fs.createReadStream
    • Dostępne do zapisu, takie jak fs.createWriteStream
    • Duplex – możliwość jednoczesnego odczytu i zapisu, jak w przypadku gniazda TCP
    • Transform – strumień typu duplex, który modyfikuje dane w trakcie ich przepływu, np. zlib.createGzip

    Q20. Co to jest bufor?

    Bufer przechowuje surowe dane binarne w pamięci przydzielonej poza pulą V8. Spotykamy go tam, gdzie dane są reprezentowane jako bajty, a nie łańcuchy lub obiekty: w strumieniach, plikach, gniazdach. Stworzenie bufora z łańcucha pokazuje podstawowe wartości bajtów w formacie szesnastkowym:

    const buf = Buffer.from('Hello');
    console.log(buf); // <Buffer 48 65 6c 6c 6f>
    

    Q21. Co to jest EventEmitter?

    EventEmitter z modułu events implementuje wzorzec obserwatora (pub/sub), na którym opiera się duża część API Node.js. Tworzy się instancję tego obiektu:

    const EventEmitter = require('events');
    const emitter = new EventEmitter();
    

    Następnie rejestruje się słuchacze za pomocą metody on, a potem uruchamia je za pomocą emit, przekazując przy tym wszelkie argumenty:

    emitter.on('greet', (name) => console.log(`Hello, ${name}`));
    emitter.emit('greet', 'John');
    

    Słuchacze są wykonywane synchronicznie, w kolejności rejestracji, gdy zostanie wywołana funkcja emit.

    Q22. Jak przetwarzać duże ilości danych bez wyczerpania pamięci?

    Należy używać strumieni: czytać małe fragmenty i przekierowywać strumień do odczytu do strumienia do zapisu, dzięki czemu zużycie pamięci pozostaje mniej więcej stałe, niezależnie od rozmiaru pliku:

    fs.createReadStream('input.txt').pipe(fs.createWriteStream('output.txt'));
    

    Dla kodu produkcyjnego stream.pipeline jest lepszy od .pipe(), ponieważ przenosi błędy i usuwa wszystkie strumienie z łańcucha.

    Głębsze koncepcje związane z czasem wykonywania

    Q23. process.nextTick() kontra setImmediate(), szczegółowo

    nextTick – funkcje zwrotne są wykonywane przed kontynuacją pętli, nawet przed mikrozadaniami typu promise. Funkcje zwrotne setImmediate czekają na fazę sprawdzania, po fazie przeglądania. Ponieważ każdy nextTick może zaplanować kolejny, łańcuch rekurencyjny może „zahamować” pętlę, przez co oczekujące operacje odczytu i zapisu nigdy nie zostaną obsłużone. Ten tryb awarii jest omówiony w artykule jak process.nextTick w tajemnicy „zahamowuje” pętlę zdarzeń w Node.js.

    Q24. Co to jest klastrowanie i dlaczego je używać?

    Jeden proces Node wykorzystuje jeden rdzeń do obsługi JavaScripta. Moduł cluster tworzy kilka procesów roboczych, z których każdy ma własną pętlę zdarzeń i pamięć, a jednocześnie umożliwia im współdzielenie jednego portu serwerowego, dzięki czemu wszystkie rdzenie maszyny mogą obsługiwać ruch. Moduły te importuje się w następujący sposób:

    const cluster = require('cluster');
    const os = require('os');
    

    Następnie z procesu głównego tworzy się po jednym pracowniku na każdy CPU, podczas gdy pracownicy ładowają serwer:

    if (cluster.isMaster) {
      os.cpus().forEach(() => cluster.fork());
    } else {
      require('./server');
    }
    

    cluster.isMaster nadal funkcjonuje, ale został uznany za przestarzały na rzecz cluster.isPrimary; w nowym kodzie należy używać tego drugiego.

    Q25. cluster versus worker_threads

    • cluster tworzy oddzielne procesy z izolowaną pamięcią, co jest odpowiednie do skalowania serwerów HTTP.
    • worker_threads tworzy wątki w obrębie jednego procesu. Mogą one dzielić się pamięcią za pomocą SharedArrayBuffer lub przekazywać bufory, co nadaje się do zadań wymagających dużych zasobów CPU, takich jak przetwarzanie obrazów.

    Q26. Jak radzi sobie z zadaniami wymagającymi dużych zasobów CPU?

    Kod obciążający CPU blokuje pętlę zdarzeń, dlatego należy przenieść go w inne miejsce: do worker_threads, do procesu potomnego lub całkowicie poza usługę za pomocą kolejki i pracowników w tle. Szukaj wzorów w artykule offloading CPU work with worker threads and pools.

    Q27. Czym jest middleware w ogólności?

    Middleware to kod znajdujący się pomiędzy otrzymaniem żądania a wysłaniem odpowiedzi, gdzie może on sprawdzać, modyfikować, walidować lub rejestrować dane. Cała architektura Express opiera się na tej koncepcji, jak pokazują poniższe sekcje dotyczące Express.

    Q28. Czym są procesy potomne?

    Moduł child_process uruchamia zewnętrzne programy lub inne skrypty Node w oddzielnych procesach. Istotne są cztery funkcje:

    • spawn() przekazuje wyniki w formie strumieni, dzięki czemu dobrze radzi sobie z dużymi ilościami danych.
  • exec() uruchamia shell i buforuje wyniki, co jest odpowiednie dla małych ilości danych.
  • execFile() uruchamia plik bezpośrednio, bez użycia shella, co jest bezpieczniejsze przy niepewnych argumentach.
  • fork() to specjalizowana wersja spawn dla modułów Node, która tworzy kanał IPC do przekazywania wiadomości.
  • Q29. Co to jest libuv?

    libuv to biblioteka w języku C, która dostarcza pętlę zdarzeń Node, asynchroniczną obsługę wejścia/wyjścia oraz zbiór wątków, ukrywając różnice pomiędzy mechanizmami powiadamiania o zdarzeniach w systemach Linux, macOS i Windows.

    Q30. setTimeout versus setInterval

    setTimeout(fn, delay) uruchamia funkcję fn raz po upływie co najmniej delay milisekund. setInterval(fn, delay) uruchamia ją wielokrotnie w tym interwale, dopóki nie zostanie wywołana funkcja clearInterval().

    Q31. Obsługa nieprzechwyconych wyjątków i nierozpatrzonych odrzuceń

    Istnieją obsługi na poziomie procesu dla obu przypadków. W przypadku synchronicznych wyjątków, których nikt nie przechwycił, należy zalogować błąd i zakończyć działanie procesu, ponieważ może on znajdować się w niespójnym stanie:

    process.on('uncaughtException', (err) => {
      console.error('Uncaught Exception:', err);
      process.exit(1); // recommended: exit after cleanup
    });
    

    W przypadku obietnic odrzuconych bez obsługi:

    process.on('unhandledRejection', (reason, promise) => {
      console.error('Unhandled Rejection:', reason);
    });
    

    Od wersji Node 15 nierozpatrzone odrzucenie domyślnie powoduje awarię procesu, więc rejestracja tego słuchacza zmienia to zachowanie. Traktuj oba przypadki jako rozwiązanie ostateczne w postaci logowania, a nie jako element przepływu sterowania; używaj try/catch oraz .catch() bezpośrednio w kodzie.

    Q32. Co to jest wyciek pamięci i jak go znaleźć?

    Ucieczka pamięci to taka pamięć, która pozostaje dostępna po tym, jak przestała być potrzebna: rosnące globalne bufory, timerzy lub słuchacze, których nigdy nie usunięto, oraz zamknięcia przechowujące duże obiekty. Diagnozuj to za pomocą flagi --inspect i zrzutów pamięci w Chrome DevTools, lub narzędzi takich jak heapdump i clinic.js, porównując zrzuty w czasie.

    Q33. __dirname versus __filename

    __dirname to absolutna ścieżka do folderu zawierającego bieżący moduł; __filename to absolutna ścieżka do samego pliku. Obie istnieją tylko w CommonJS. W modułach ES uzyskuj je z import.meta.url, lub użyj import.meta.dirname i import.meta.filename w nowszych wersjach Node.

    Q34. Dlaczego używać modułu path?

    path umożliwia tworzenie i analizę ścieżek plików w sposób uniwersalny za pomocą funkcji takich jak path.join, path.resolve, path.basename oraz path.extname, dzięki czemu nie musisz ręcznie łączyć ciągów znaków i mierzyć się z różnicami między ukośnikami Windows a Unix.

    Q35. fs.readFile versus fs.createReadStream

    fs.readFile ładuje cały plik do pamięci przed zwróceniem wyniku, co stanowi ryzyko przy dużych plikach. fs.createReadStream dostarcza plik w fragmentach, utrzymując niski zużycie pamięci i umożliwiając natychmiastowe rozpoczęcie przetwarzania.

    Podstawy Express

    Q36. Co to jest Express?

    Express to minimalistyczna, bezstronna ramka robocza dla stron internetowych oparta na module http w Node. Dodaje funkcje routingu, pipeline middleware oraz integrację z silnikiem szablonów, eliminując większość standardowych elementów związanych z obsługą żądań.

    Q37. Podstawowy serwer Express

    Stwórz obiekt aplikacji:

    const express = require('express');
    const app = express();
    

    Następnie zarejestruj trasę i rozpocznij odsłuchiwanie:

    app.get('/', (req, res) => {
      res.send('Hello World');
    });app.listen(3000, () => console.log('Server running on port 3000'));
    

    Q38. Co to jest middleware w Express?

    Middleware w Express to funkcja, która otrzymuje parametry req, res oraz next. Może wykonywać dowolny kod, modyfikować żądanie lub odpowiedź, zakończyć proces odpowiedzią lub wywołać next(), aby przekazać kontrolę następnej funkcji. Klasycznym przykładem jest logger żądań:

    app.use((req, res, next) => {
      console.log(`${req.method} ${req.url}`);
      next();
    });
    

    Jeśli middleware nie odpowiada ani nie wywołuje next(), żądanie utyka.

    Q39. Rodzaje middleware w Express

    1. Na poziomie aplikacji, przytwierdzane za pomocą app.use() lub app.METHOD().
    2. Na poziomie routera, przytwierdzane do instancji express.Router().
  • Obsługa błędów, rozpoznawalna po czterech parametrach (err, req, res, next).
  • Budowane wewnętrznie funkcje, takie jak express.json() i express.static().
  • Biblioteki zewnętrzne, takie jak cors, morgan i helmet.
  • Q40. Jak działa routowanie?

    Trasa łączy metodę HTTP z ścieżką oraz obsługującym ją skryptem. Typowa zasoba typu CRUD wygląda w ten sposób:

    app.get('/users', (req, res) => { /* get all users */ });
    app.post('/users', (req, res) => { /* create user */ });
    app.put('/users/:id', (req, res) => { /* update user */ });
    app.delete('/users/:id', (req, res) => { /* delete user */ });
    

    Q41. Co to jest express.Router()?

    Router to samodzielna mini-aplikacja z własnymi trasami i middleware, którą montuje się na określonej ścieżce. Umożliwia uporządkowanie dużych aplikacji według funkcjonalności. Trasy definiuje się w osobnym module:

    // userRoutes.js
    const router = require('express').Router();
    router.get('/', (req, res) => res.send('All users'));
    module.exports = router;
    

    a następnie montuje się go w głównej aplikacji, gdzie ścieżka / staje się /users:

    // app.js
    app.use('/users', require('./userRoutes'));
    

    Q42. app.use() w porównaniu z app.get() i podobnymi funkcjami

    app.use() pasuje do każdej metody HTTP oraz do dowolnej ścieżki zaczynającej się od podanego prefiksu, dlatego właśnie montuje middleware i routery. app.get(), app.post() oraz podobne funkcje pasują do jednej konkretnej metody i pełnego wzoru ścieżki.

    Q43. req.params, req.query i req.body

    • req.params przechowuje nazwane segmenty ścieżki: /users/:id daje req.params.id.
    • req.query przechowuje wartości ciągu zapytania: /users?age=25 daje req.query.age, zawsze w formie ciągu znaków.
    • req.body przechowuje zinterpretowany treść żądania dla metod takich jak POST i PUT; pozostaje nieskończony, chyba że najpierw zostanie uruchomiony middleware do analizy treści, np. express.json().

    Q44. Analiza JSON i danych formularza

    Wbudowane parserzy obejmują większość przypadków – jeden do treści JSON, a drugi do form HTML z kodowaniem URL:

    app.use(express.json());               // parses application/json
    app.use(express.urlencoded({ extended: true })); // parses form data
    

    Q45. Co to jest express.static()?

    Jest to wbudowany middleware, który serwuje pliki takie jak obrazy, arkusze stylów, skrypty i HTML z określonego katalogu:

    app.use(express.static('public'));
    

    Średni poziom Express

    Q46. Obsługa błędów

    Błędy są obsługiwane za pomocą middleware’u z czterema parametrami, zarejestrowanego po wszystkich trasach:

    app.use((err, req, res, next) => {
      console.error(err.stack);
      res.status(500).json({ message: 'Something went wrong' });
    });
    

    Wewnątrz tras i middleware’ów przekazuj błędy za pomocą next(err):

    app.get('/user/:id', async (req, res, next) => {
      try {
        const user = await getUser(req.params.id);
        res.json(user);
      } catch (err) {
        next(err); // passed to error-handling middleware
      }
    });
    

    Unikaj wysyłania err.stack lub informacji wewnętrznych do klientów w środowisku produkcyjnym.

    Q47. Obsługa błędów 404

    Express nie posiada wbudowanej obsługi błędów 404 w formacie JSON. Dodaj middleware ogólny po każdej trasie; jeśli żądanie trafi do niego, nic innego się nie dopasowało:

    app.use((req, res) => {
      res.status(404).json({ message: 'Route not found' });
    });
    

    Q48. CORS i pakiet cors

    Cross-Origin Resource Sharing to mechanizm przeglądarki, który decyduje, czy strona z jednego źródła może odczytywać odpowiedzi z innego. Przeglądarka to egzekwuje; serwer decyduje o tym poprzez nagłówki odpowiedzi, które pakiet cors ustawia za Ciebie:

    const cors = require('cors');
    app.use(cors()); // allow all origins
    app.use(cors({ origin: 'https://example.com' })); // restrict origin
    

    Ukazano tu dwie alternatywy: możliwość korzystania przez wszystkie źródła lub tylko jedno; należy wybrać jedną z nich, a nie obie, ponieważ pierwsza opcja będzie miała priorytet.

    Q49. Sesje i pliki cookie

    Włóż środowisko pośredniczące do obsługi plików cookie i sesji:

    const cookieParser = require('cookie-parser');
    const session = require('express-session');
    

    następnie zarejestruj je przy użyciu tajemniczego klucza podpisywania oraz odpowiednich opcji:

    app.use(cookieParser());
    app.use(session({
      secret: 'mySecretKey',
      resave: false,
      saveUninitialized: true
    }));
    

    Zachowaj tę tajemnicę w zmiennej środowiskowej, a nie w kodzie źródłowym, i rozważ ustawienie saveUninitialized: false, aby uniknąć tworzenia sesji dla anonimowych odwiedzających. Standardowe przechowywanie w pamięci nie jest przeznaczone do użycia w środowisku produkcyjnym.

    Q50. Autoryzacja JWT

    Importuj bibliotekę:

    const jwt = require('jsonwebtoken');
    

    Poniższy kod podpisuje token zawierający identyfikator użytkownika przy logowaniu, chroni ścieżki za pomocą middleware, który wyodrębnia token nośny z nagłówka Authorization i sprawdza go, a następnie przypisuje zdekodowany payload do req.user:

    // Generate token on login
    const token = jwt.sign({ id: user.id }, 'secretKey', { expiresIn: '1h' });// Middleware to protect routes
    function auth(req, res, next) {
      const token = req.headers.authorization?.split(' ')[1];
      if (!token) return res.status(401).json({ message: 'No token' });
      try {
        req.user = jwt.verify(token, 'secretKey');
        next();
      } catch (err) {
        res.status(403).json({ message: 'Invalid token' });
      }
    }app.get('/protected', auth, (req, res) => res.json({ data: 'secret data' }));
    

    Tak jak w przypadku sesji, stała wartość 'secretKey' powinna pochodzić z konfiguracji. Odpowiedź 401 w przypadku braku tokena oraz 403 w przypadku nieprawidłowego tokena to powszechna konwencja, chociaż wiele API zwraca 401 w obu przypadkach.

    Q51. Walidacja żądania

    Biblioteki takie jak express-validator lub joi definiują reguły i zbierają błędy. Z express-validator importuje się pomocniki:

    const { body, validationResult } = require('express-validator');
    

    Następnie łączy się middleware z regułami przed obsługą żądania i sprawdza wynik:

    app.post('/user',
      body('email').isEmail(),
      body('password').isLength({ min: 6 }),
      (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
        // proceed
      }
    );
    

    Aby dokonać bardziej szczegółowego porównania metod walidacji, zobacz Zod versus express-validator.

    Q52. Wgrzewanie plików

    multer parsuje dane typu multipart/form-data. Konfiguruj miejsce przechowywania plików:

    const multer = require('multer');
    const upload = multer({ dest: 'uploads/' });
    

    i zastosuj to dla każdej trasy; upload.single('file') oczekuje jednego pliku w polu o nazwie file i udostępnia go jako req.file:

    app.post('/upload', upload.single('file'), (req, res) => {
      res.json({ file: req.file });
    });
    

    W rzeczywistych systemach należy również ograniczyć rozmiar i typ plików, a także nigdy nie ufać nazwie pliku dostarczonej przez klienta.

    Q53. Połączenie z MongoDB za pomocą Mongoose

    Nawiąż połączenie:

    const mongoose = require('mongoose');
    mongoose.connect('mongodb://localhost:27017/mydb');
    

    następnie zdefiniuj model i użyj go w danej trasie:

    const User = mongoose.model('User', new mongoose.Schema({
      name: String,
      email: String
    }));app.post('/users', async (req, res) => {
      const user = await User.create(req.body);
      res.status(201).json(user);
    });
    

    Przekazywanie bezpośrednio req.body do funkcji create pozwala klientom ustawiać dowolne pola schematu, dlatego należy najpierw zweryfikować dane lub wybrać tylko dozwolone pola.

    Q54. Kolejność wykonywania middleware

    Express uruchamia middleware w kolejności, w jakiej zostały one zarejestrowane, od góry do dołu, a łańcuch zatrzymuje się przy pierwszej funkcji, która nie wywołuje next(). To właśnie z tego powodu obsługa błędów 404 i inne mechanizmy obsługi błędów znajdują się na końcu.

    Q55. Silniki szablonów

    Silniki szablonów renderują HTML na podstawie danych po stronie serwera. Popularnymi opcjami są EJS, Pug (wcześniej Jade) oraz Handlebars. Aby je uruchomić, potrzebna jest konfiguracja silnika szablonów oraz wywołanie funkcji res.render:

    app.set('view engine', 'ejs');
    app.get('/', (req, res) => res.render('index', { name: 'John' }));
    

    Zaawansowane zastosowania Express i Node

    Q56. Błędy asynchroniczne w Express 4 w porównaniu z Express 5

    W Express 4 odrzucona obietnica z obsługiwanego elementu async nie jest przechwytywana przez framework; bez bloków try/catch w połączeniu z wywołaniem next(err) lub odpowiedniego otulacza, staje się odrzuceniem nierozwiązанныm. Express 5 automatycznie przekazuje odrzucone obietnice z elementów obsługujących i middleware do mechanizmu obsługi błędów. Typowym rozwiązaniem w Express 4 jest mała funkcja wyższego rzędu:

    // Express 4 workaround
    const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
    

    Otulenie elementu obsługującego taką funkcją kieruje każde odrzucenie do wywołania next:

    app.get('/data', asyncHandler(async (req, res) => {
      const data = await fetchData(); // if this throws, asyncHandler catches it
      res.json(data);
    }));
    

    Ten wzorzec jest szczegółowo opisany w artykule usuwanie standardowego kodu try/catch za pomocą asyncHandler.

    Q57. Strukturyzacja dużej aplikacji Express

    Częsty układ w stylu MVC dzieli różne aspekty aplikacji na osobne foldery:

    project/
    ├── controllers/     # business logic
    ├── routes/          # route definitions
    ├── models/          # DB schemas
    ├── middlewares/      # custom middleware
    ├── config/          # DB, env configs
    ├── utils/           # helper functions
    ├── app.js
    └── server.js
    

    Trasy deklarują punkty końcowe i przekazują je do kontrolerów, które zawierają logikę obsługi żądań, natomiast modele odpowiadają za zabezpieczenie dostępu do danych. Rozdzielenie pliku app.js (konfiguracja aplikacji) od pliku server.js (uruchamianie jej) ułatwia również importowanie aplikacji w testach.

    Q58. Ograniczanie szybkości

    express-rate-limit liczy żądania od danego klienta w określonym czasie. Należy go zaimportować:

    const rateLimit = require('express-rate-limit');
    

    i zastosować ogranicznik – w tym przypadku 100 żądań na IP co 15 minut:

    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100 // limit each IP to 100 requests per window
    });app.use(limiter);
    

    Standardowy magazyn danych znajduje się w pamięci, więc każdy proces liczy żądania oddzielnie; przy skalowaniu należy użyć wspólnego magazynu, takiego jak Redis. Za proxyem konieczna jest konfiguracja opcji trust proxy, aby użyto rzeczywistego IP klienta.

    Q59. Zabezpieczanie aplikacji Express

    • Ustaw headery HTTP o charakterze ochronnym za pomocą biblioteki helmet.
  • Waliduj i oczyszczaj wszystkie dane wejściowe, aby zapobiec iniekcjom SQL lub NoSQL oraz atakom XSS.
  • Servir przez HTTPS.
  • Zastosuj ograniczenia szybkości wysyłania żądań.
  • Ładuj dane poufne z środowiska, na przykład za pomocą dotenv, zamiast przechowywać je w kodzie.
  • Zapewnij aktualizacje zależności i uruchom npm audit.
  • Dodaj ochronę CSRF dla aplikacji opartych na plikach cookie i formularzach.
  • Q60. res.send() versus res.json() versus res.end()

    • res.send() przyjmuje łańcuchy, obiekty lub buforzy i wybiera odpowiedni Content-Type.
    • res.json() serializuje swój argument jako JSON i ustawia Content-Type: application/json.
    • res.end() zakończa odpowiedź bez dodatkowych danych – jest to uproszczona warstwa otaczająca wbudowaną metodę Node.

    Q61. Co robi next('route')?

    Pomija pozostałe funkcje powrotnicze bieżącej trasy i przechodzi do następnej pasującej trasy. Dotyczy to wyłącznie obsługowników zarejestrowanych za pomocą app.METHOD() lub router.METHOD(), które mają kilka funkcji powrotniczych.

    Q62. Poprawa wydajności

    • Zastosuj klastrowanie lub PM2, aby uruchamiać po jednym procesie na każdym rdzeniu.
    • Zachowuj odpowiedzi w pamięci podręcznej Redis lub w pamięci komputera.
    • Skompresuj odpowiedzi za pomocą middleware’u compression lub zleć to proxyowi.
    • Umieść przed aplikacją Nginx lub inny proxy odwrotny lub balanser obciążeń.
    • Unikaj wykonywania zadań wymagających dużo mocy obliczeniowej CPU w pętli zdarzeń.
    • Wykorzystaj pulę połączeń do bazy danych.
    • Rozważ użycie protokołu HTTP/2.
    • Zastosuj asynchroniczną obsługę wejścia/wyjścia we wszystkich miejscach i unikaj wariantów Sync w ścieżkach żądań.

    Q63. REST kontra GraphQL z Express

    REST udostępnia wiele punktów końcowych o ustalonych strukturach odpowiedzi, dlatego klienty często pobierają zbyt dużo lub zbyt mało danych. GraphQL udostępnia jeden punkt końcowy, w którym klient precyzyjnie określa dane, które chce otrzymać. Zazwyczaj jest on integrowany z Express za pomocą express-graphql lub apollo-server-express; oba te pakiety zostały już uznane za przestarzałe na rzecz nowszych rozwiązań, dlatego należy sprawdzić aktualną dokumentację Apollo lub GraphQL Yoga.

    Q64. Jak Express radzi sobie z równoczesnymi żądaniami?

    Każda prośba to po prostu seria funkcji zwrotnych w pętli zdarzeń. Ponieważ operacje na bazie danych i sieci nie są blokujące, jeden wątek może obsługiwać wiele żądań jednocześnie: ruch sieciowy jest przekazywany do mechanizmów asynchronicznych systemu operacyjnego, praca z plikami do puli wątków libuv, a funkcje zwrotne są wykonywane po otrzymaniu wyników. W przeciwieństwie do klasycznych serwerów wielowątkowych, nie ma jednego wątku na żądanie, i to dlatego jeden wolny, synchroniczny obsługa wpływa na wszystkich.

    Q65. PM2 versus moduł cluster

    cluster to wbudowana funkcja do skalowania wieloprocesowego. PM2 to menedżer procesów oparty na tej samej koncepcji, który dodaje automatyczne restarty po awariach, równoważenie obciążenia, obsługę logów, ładowanie bez przerwy oraz monitorowanie, dlatego jest używany w tak wielu produkcjach.

    Q66. Dlaczego umieszczać proxy odwrotny przed Node?

    Proxy odwrotny, takie jak Nginx, kończy proces TLS, równomiernie rozkłada obciążenie między instancje, cacheuje dane, kompresuje pliki i serwuje pliki statyczne, co uwalnia Node od tych zadań, aby proces aplikacji mógł skupić się na logice biznesowej.

    Q67. Konfiguracja specyficzna dla środowiska

    Pakiet dotenv ładuje zmienne z pliku .env do process.env, a aplikacja korzysta z wartości domyślnych, gdy dana wartość jest brakująca:

    require('dotenv').config();
    const PORT = process.env.PORT || 3000;
    

    Nowsze wersje Node mogą również ładować pliki env natywnie za pomocą flagi --env-file. Nigdy nie przechowuj w repozytorium plików .env zawierających dane poufne.

    Q68. app.all() versus app.use()

    app.all() pasuje do każdej metody HTTP na określonej ścieżce. app.use() również pasuje do każdej metody, ale do całej ścieżki i wszystkiego poniżej niej, dlatego jest narzędziem do montowania middleware.

    Q69. Paginacja z Express i MongoDB

    Przeczytaj wartości page i limit z ciągu zapytania, używając rozsądnych domyślnych wartości, a następnie połącz je z skip:

    app.get('/posts', async (req, res) => {
      const page = parseInt(req.query.page) || 1;
      const limit = parseInt(req.query.limit) || 10;
      const posts = await Post.find()
        .skip((page - 1) * limit)
        .limit(limit);
      res.json(posts);
    });
    

    Zabezpiecz to przed wdrożeniem: przekaż radix do parseInt, ogranicz page do co najmniej 1, a limit do maksymalnej wartości, oraz dodaj stabilną opcję sort. W przypadku bardzo dużych kolekcji szybkość operacji skip spada wraz ze wzrostem wartości offsetu, natomiast paginacja oparta na kursorze w polu zindeksowanym radzi sobie lepiej.

    Q70. Co to jest idempotencja?

    Operacja jest idempotentna, gdy jej powtórzenie pozostawia serwer w tym samym stanie co wykonanie jej raz. Operacje GET, PUT i DELETE są definiowane jako idempotentne; POST zazwyczaj nie jest, ponieważ każde wezwanie może stworzyć nowy zasób. Ponawianie prób po wygaśnięciu czasu oczekiwania jest bezpieczne tylko w przypadku zapytań idempotentnych, dlatego API płatności często dodają klucze idempotencji do zapytań typu POST.

    Główne wnioski

    • Większość pytań dotyczących Node sprowadza się do jednego modelu: pojedynczej wątku JavaScript, pętli zdarzeń z określonymi fazami, mikrozadań usuwanych po każdym callbacku oraz biblioteki libuv zajmującej się resztą.
    • Większość pytań dotyczących Express sprowadza się do innego modelu: uporządkowanej łańcuchowej struktury middleware, w której każda funkcja albo odpowiada, albo wywołuje next(), a błędy trafiają do obsługujących je funkcji przyjmujących cztery argumenty.
  • Pracodawcy doceniają dokładność w odniesieniu do szczegółów, które zmieniały się z upływem czasu, takich jak cluster.isPrimary, błędy spowodowane nieobsłużonymi odrzuceniami, asynchroniczne błędy w Express 5 oraz narzędzia do obsługi ścieżek w ESM.
  • Do każdej odpowiedzi należy dodać uwagę dotyczącą warunków produkcji (tajne dane w konfiguracji, propagacja błędów, wspólne zasoby do ograniczania liczby żądań), ponieważ to zazwyczaj jest kolejne pytanie.