Siedem zadań programistycznych dla początkujących z Node.js i Express, rozwiązanych i wyjaśnionych
Przepracuj siedem typowych zadań z rozmów rekrutacyjnych dla początkujących programistów Node.js, od tras Express i middleware po obsługę błędów, odwracanie ciągów znaków i wykrywanie duplikatów, oraz naucz się tłumaczyć każde z nich.
Podczas rozmów kwalifikacyjnych z osobami początkującymi w Node.js i Express rzadko chodzi o skomplikowane algorytmy. Zazwyczaj dostajesz proste zadania, takie jak utworzenie trasy, napisanie middleware’a lub oczyszczenie tablicy, podczas gdy rozmówca obserwuje, jak rozumujesz i czy potrafisz wyjaśnić, dlaczego twój kod działa. Siedem poniższych zadań obejmuje najczęściej występujące tematy, każde z nich zawiera działające rozwiązanie, opis kluczowych linijek kodu oraz pytania dodatkowe, które pokazują prawdziwe zrozumienie tematu.
Zadanie 1: Stwórz podstawową trasę GET
Klasyczne ćwiczenie wprowadzające: „Utwórz punkt dostępu GET pod adresem /, który odpowiada tekstem Hello World”. Rozwiązanie polega na utworzeniu aplikacji Express, zarejestrowaniu obsługi dla ścieżki korzeniowej i rozpoczęciu odsłuchiwania na określonym porcie.
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.status(200).send("Hello World");
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
Ponieważ używa się import, projekt wymaga wpisu "type": "module" w pliku package.json lub rozszerzenia .mjs.
Czytanie linii trasy po kolei
Wezwanie app.get() rejestruje obsługę, która reaguje wyłącznie na żądania typu GET:
app.get("/", (req, res) => {
Pierwszy argument, /, to ścieżka. Funkcja zwrotna otrzymuje req, który opisuje przychodzące żądanie, oraz res, używany do tworzenia odpowiedzi.
Sama odpowiedź jest generowana tutaj:
res.status(200).send("Hello World");
status(200) ustawia kod stanu (200 jest wartością domyślną, ale jasne określenie jest lepiej czytelne), a send() zapisuje treść i kończy odpowiedź.
Na koniec uruchamiany jest serwer:
app.listen(3000, () => {
To powiązuje aplikację z portem 3000; funkcja callback jest wywoływana po tym, jak serwer przyjmuje połączenia. Prawdziwe usługi zazwyczaj odczytują numer portu z process.env.PORT.
Jak to ująć
"
app.get()rejestruje ścieżkę dla żądań typu GET. Gdy klient wywoła/, Express uruchamia funkcję callback, która odpowiada ze statusem 200 i tekstem Hello World."
Zadanie 2: Napisz middleware do logowania żądań
Kolejne polecenie brzmi: ">Stwórz middleware, który loguje każde przychodzące żądanie." Middleware to funkcja o sygnaturze (req, res, next). Ta konkretna funkcja wyświetla metodę i adres URL, a następnie przekazuje kontrolę dalej; app.use() stosuje ją do wszystkich zarejestrowanych po niej ścieżek.
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
};
app.use(logger);
Każde żądanie przechodzi kolejno przez łańcuch przed tym, jak obsługa ścieżki udzieli odpowiedzi:
Client
↓
Request
↓
Logger Middleware
↓
Route Handler
↓
Response
Najważniejszym wierszem jest ostatni wewnątrz funkcji:
next();
next() informuje Express, że ten krok został zakończony i powinien zostać uruchomiony następny middleware lub ścieżka. Jeśli o to zapomnieć, żądanie pozostaje w zawieszeniu aż klient wygaśnie – to częste pytanie. Alternatywą jest zakończenie żądania poprzez wysłanie odpowiedzi, tak jak robi to middleware autoryzacyjny, gdy odrzuca brakujący token.
Jak to ująć
"Middleware otrzymuje żądanie, odpowiedź oraz funkcję
next. Może sprawdzić lub zmodyfikować żądanie, a następnie albo odpowiedzieć, albo wywołaćnext(), aby przekazać kontrolę dalej."
Zadanie 3: Obsługa pracy asynchronicznej w ścieżce
Prawdziwe punkty końcowe czekają na dane z baz danych i innych API, więc można się spodziewać pytania „Jak radzić sobie z operacją asynchroniczną w Express?” Solidną odpowiedzią jest obsługa typu async z blokiem try/catch:
app.get("/users", async (req, res, next) => {
try {
const users = await getUsers();
res.status(200).json(users);
} catch (error) {
next(error);
}
});
Kluczowym elementem jest instrukcja await:
const users = await getUsers();
Zatrzymuje ona działanie tej obsługi, dopóki getUsers() nie zakończy się pomyślnie, bez blokowania innych żądań. Jeśli obietnica zostanie odrzucona, uruchamia się blok catch:
catch (error) {
next(error);
}
next(error) przekazuje błąd do middleware obsługującego błędy z Zadania 5. Warto dodać, że Express 4 nie przechwytuje odrzuconych obietnic z obsług asynchronicznych, więc tam blok try/catch jest niezbędny, podczas gdy Express 5 przekazuje je automatycznie. Aby dowiedzieć się więcej, zapoznaj się z budową obsługi błędów na poziomie produkcyjnym w aplikacjach Node.js.
Zadanie 4: Narysuj punkty końcowe CRUD
CRUD oznacza Create, Read, Update i Delete – cztery podstawowe operacje na danych, z których każda odpowiada określonej metodzie HTTP. Przykłady punktów końcowych dla zasobu users:
app.post("/users", (req, res) => {
res.status(201).send("User created");
});
app.get("/users", (req, res) => {
res.status(200).send("Users");
});
app.put("/users/:id", (req, res) => {
res.status(200).send("User updated");
});
app.delete("/users/:id", (req, res) => {
res.status(200).send("User deleted");
});
Proces tworzenia zwraca 201 Created, a trasy dotyczące pojedynczego użytkownika przyjmują parametr :id pobrany z req.params.id. Zamiast zapamiętywać konkretne linie kodu, należy znać znaczenie każdej metody:
POST → Create
GET → Read
PUT → Update
DELETE → Delete
Bądź przygotowany na różnicę między PUT a PATCH: PUT tradycyjnie zastępuje cały zasób, natomiast PATCH wprowadza tylko częściową zmianę.
Jak to ująć
"CRUD obejmuje cztery podstawowe operacje na danych. W REST POST służy do tworzenia, GET do odczytu, PUT lub PATCH do aktualizacji, a DELETE do usuwania."
Zadanie 5: Centralizacja obsługi błędów
Obsługa błędów zazwyczaj następuje po Zadaniu 3. Zacznij od trasy, która zawodzi i przekazuje błąd:
app.get("/", (req, res, next) => {
try {
throw new Error("Something went wrong");
} catch (error) {
next(error);
}
});
Następnie dodaj obsługę błędów, która zapisuje informacje o błędzie i zwraca ogólny kod 500:
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).json({
message: "Internal Server Error"
});
});
To, co czyni tę funkcję wyjątkową, to jej sygnatura:
(err, req, res, next)
Express rozpoznaje obsługi błędów po czterech zadeklarowanych parametrach, w kolejności err, req, res, next; są one wykonywane tylko wtedy, gdy przekazany został błąd. Zarejestruj obsługę po swoich trasach i unikaj wysyłania err.message do klientów w środowisku produkcyjnym. Przepływ działania:
Route
↓
Error occurs
↓
catch(error)
↓
next(error)
↓
Error middleware
↓
500 Response
Czy wewnątrz obsługi błędów konieczne jest użycie next()?
Nie, jeśli obsługa sama wysyła odpowiedź:
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).send("Internal Server Error");
});
next musi nadal być zadeklarowany, ponieważ przy trzech parametrach Express traktuje funkcję jako zwykły middleware.
Zadanie 6: Odwrócenie ciągu znaków
Część zadań polega po prostu na użyciu JavaScripta: „Odwróć ciąg hello”. Idiomaticzne rozwiązanie jednolinijowe łączy trzy metody:
const str = "hello";
const reversed = str.split("").reverse().join("");
console.log(reversed);
Wydrukuje ono:
olleh
Co robi każdy krok
Funkcja split daje jako wynik znaki:
str.split("")
Wynikiem jest tablica:
["h", "e", "l", "l", "o"]
Następnie kolejność elementów jest odwracana na miejscu:
.reverse()
To daje:
["o", "l", "l", "e", "h"]
Na koniec znaki są łączone bez żadnego separatora:
.join("")
W ten sposób powstaje odwrócony ciąg:
"olleh"
Powszechna modyfikacja zadania: „Teraz zrób to bez użycia reverse()”. Pętla przechodzi od ostatniego indeksu w dół do zera:
const str = "hello";
let reversed = "";
for (let i = str.length - 1; i >= 0; i--) {
reversed += str[i];
}
console.log(reversed);
Oba podejścia są liniowe. Warto zaznaczyć, że split("") oraz indeksowanie działają na jednostkach kodu UTF-16, więc emoji są rozdzielane; natomiast [...str] dzieli ciąg według punktów kodu.
Zadanie 7: Znalezienie duplikatów w tablicy
Kolejny ulubiony przykład: „Zwróć wartości, które występują więcej niż raz”. Dane wejściowe:
const numbers = [1, 2, 3, 2, 4, 3, 5];
Oczekiwany wynik to:
[2, 3]
Użyj dwóch obiektów Set – jednego dla wartości już widzianych, a drugiego dla potwierdzonych duplikatów:
const numbers = [1, 2, 3, 2, 4, 3, 5];
const seen = new Set();
const duplicates = new Set();
for (const number of numbers) {
if (seen.has(number)) {
duplicates.add(number);
} else {
seen.add(number);
}
}
console.log([...duplicates]);
Podczas uruchomienia programu pojawia się następujący log:
[2, 3]
Dlaczego dwie tablice typu Set działają skutecznie
seen rejestruje każdą przekazaną wartość. Pierwsze wystąpienie jest tam dodawane:
seen.add(number);
Ponowne wystąpienie powoduje pomyślne przejście tej sprawdzenia:
if (seen.has(number))
Następnie wartość trafia do tablicy duplicates, a ponieważ Set ignoruje powtórzenia, jest ona raportowana tylko raz. Zapytania do Set odbywają się w czasie niemal stałym, więc rozwiązanie jest liniowe, a nie oparte na kwadratowych pętlach nawzajem zawartych. Aby policzyć liczba wystąpień, użyj Map.
Rozważ swoje podejście przed zapisaniem kodu
Najważniejszym nawykiem jest wyjaśnienie swojego planu przed napisaniem kodu, ponieważ rozmówcy oceniają zarówno rozumowanie, jak i wyniki. W zadaniu z duplikatami zacznij od czegoś w rodzaju:
"Plan polega na użyciu zbioru wartości, które już zostały zauważone. Podczas pętli każda wartość, która już się w nim znajduje, trafia do drugiego zbioru duplikatów, dzięki czemu każda z nich pozostaje unikalna."
Następnie napisz kod. To dowodzi zrozumienia tematu, a nie tylko zapamiętania informacji, i pozwala rozmówcy wcześnie skierować cię w odpowiednim kierunku.
Lista kontrolna przygotowań
Przed rozmową kwalifikacyjną z juniorem w Node.js lub Express upewnij się, że potrafisz swobodnie:
- Definiować trasy i wyjaśniać rolę
reqorazres - Używać
app.use()i pisać własne middleware - Wyjaśniać, co robi funkcja
next()i co się dzieje, gdy jest brakująca
async/await, try/catch oraz next(error)Set a Map do wyszukiwania i liczeniaLiteratura pokrewna
- Node.js 26: Temporal API, Map Upserts i Undici 8 wyjaśnione — Wyjaśnia główne zmiany w Node.js 26 dotyczące warstwy backendowej, w tym stabilną Temporal API, wbudowane metody Map upsert, poprawy wydajności Undici 8 oraz zmiany mogące powodować problemy, które należy sprawdzić przed aktualizacją.
- Node.js Streams wyjaśnione: Naprawa błędów przeciążenia pamięci przy plikach — Dowiedz się, dlaczego ładowanie całych plików do pamięci powoduje awarie serwerów Node.js oraz jak strumienie typu readable, writable, duplex i transform naprawiają ten problem dzięki mechanizmowi backpressure.