Jak Przygotować się do Rozmowy z Node.js - Kompletny Przewodnik

Sławomir Plamowski 16 min czytania
backend express interview-questions javascript nodejs programowanie rekrutacja

"Wyjaśnij dokładnie, jak działa event loop i dlaczego setTimeout z czasem 0 nie wykonuje się natychmiast." To pytanie potrafi zaskoczyć nawet programistów z wieloletnim doświadczeniem w Node.js. Większość developerów świetnie pisze kod i buduje działające API, ale nie potrafi wyjaśnić mechanizmów działających pod spodem.

Na rozmowach rekrutacyjnych z Node.js pojawia się powtarzający się wzorzec: kandydaci znają Express, potrafią zbudować REST API, ale nie wyjaśnią, dlaczego Node.js radzi sobie z tysiącami połączeń na jednym wątku. Nie wiedzą, jak naprawdę działa middleware ani czym różni się microtask od macrotask.

W tym artykule znajdziesz najważniejsze tematy pojawiające się na rozmowach rekrutacyjnych z Node.js. Bez egzotycznych przypadków - skupimy się na pytaniach, które naprawdę padają i które odróżniają kandydatów rozumiejących Node.js od tych, którzy tylko go używają.

1. Czym Jest Node.js i Dlaczego Jest Inny

To pytanie otwierające padające na większości rozmów. Rekruter sprawdza, czy rozumiesz fundamentalną różnicę między Node.js a tradycyjnymi serwerami.

Odpowiedź w 30 sekund

Gdy rekruter pyta "Czym jest Node.js?", odpowiadam tak:

Node.js to środowisko wykonawcze JavaScript poza przeglądarką, oparte na silniku V8 z Chrome. Kluczowa różnica w porównaniu z tradycyjnymi serwerami jak Apache to model I/O - Node jest jednowątkowy i asynchroniczny. Zamiast tworzyć nowy wątek dla każdego żądania, używa event loop do obsługi wielu operacji jednocześnie. Dzięki temu może obsłużyć tysiące połączeń przy niskim zużyciu pamięci.

Poczekaj na reakcję. Jeśli rekruter chce więcej, rozwiń.

Odpowiedź w 2 minuty

Tradycyjne serwery jak Apache działają w modelu wielowątkowym - każde żądanie dostaje własny wątek. To intuicyjne, ale kosztowne - każdy wątek zużywa pamięć, a przy tysiącach połączeń serwer może się zapchać.

Node.js podchodzi do tego inaczej. Jest jednowątkowy, ale operacje I/O (odczyt plików, zapytania do bazy, żądania sieciowe) są wykonywane asynchronicznie w tle. Gdy operacja się kończy, callback trafia do kolejki i jest wykonywany przez event loop.

Pokażę na przykładzie:

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

const server = http.createServer((req, res) => {
    // Ta operacja NIE blokuje serwera
    fs.readFile('duzy-plik.txt', (err, data) => {
        if (err) {
            res.statusCode = 500;
            res.end('Błąd serwera');
            return;
        }
        res.end(data);
    });
    // Serwer może obsługiwać inne żądania
    // podczas gdy plik jest czytany w tle
});

server.listen(3000);

Podczas gdy jeden użytkownik czeka na odczyt pliku, serwer może obsługiwać setki innych żądań. To właśnie sprawia, że Node.js jest idealny do aplikacji I/O-intensive jak API, chat, streaming.

Kiedy Node.js NIE Jest Dobrym Wyborem

Kandydaci, którzy robią dobre wrażenie, sami wspominają o ograniczeniach:

Node.js nie nadaje się do zadań CPU-intensive - intensywne obliczenia blokują event loop i cały serwer staje. Do takich zadań lepszy jest Python, Go lub worker threads w Node. Dlatego Node świetnie sprawdza się w API i real-time apps, ale nie w przetwarzaniu obrazów czy machine learning.

2. Event Loop - Serce Node.js

To temat, który odróżnia juniorów od mid-level developerów. Każdy używa async/await, ale niewielu rozumie, co dzieje się pod spodem.

Odpowiedź w 30 sekund

Event loop to mechanizm zarządzający wykonaniem kodu w Node.js. Działa w cyklu: najpierw wykonuje cały kod synchroniczny, potem sprawdza kolejkę microtasks (Promise callbacks), następnie bierze jedno zadanie z kolejki macrotasks (setTimeout, I/O callbacks). Microtasks mają wyższy priorytet - wszystkie są wykonywane przed następnym macrotask.

Fazy Event Loop

Event loop przechodzi przez kilka faz w określonej kolejności:

   ┌───────────────────────────┐
┌─>│           timers          │  <- setTimeout, setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  <- I/O callbacks z poprzedniej iteracji
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  <- wewnętrzne operacje
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │  <- pobieranie nowych I/O events
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │  <- setImmediate callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │  <- socket.on('close')
   └───────────────────────────┘

Klasyczny Problem: Kolejność Wykonania

Rekruter pokazuje kod i pyta o kolejność wypisania:

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

process.nextTick(() => console.log('4'));

console.log('5');

Odpowiedź: 1, 5, 4, 3, 2

Wyjaśnienie krok po kroku:

// 1. Kod synchroniczny wykonuje się pierwszy
console.log('1');  // Wypisuje: 1
console.log('5');  // Wypisuje: 5

// 2. process.nextTick ma najwyższy priorytet wśród asynchronicznych
// Wykonuje się PRZED microtasks
process.nextTick(() => console.log('4'));  // Wypisuje: 4

// 3. Promise.then to microtask - wykonuje się przed macrotasks
Promise.resolve().then(() => console.log('3'));  // Wypisuje: 3

// 4. setTimeout to macrotask - wykonuje się na końcu
setTimeout(() => console.log('2'), 0);  // Wypisuje: 2

Priorytet: synchroniczny kod > process.nextTick > microtasks (Promise) > macrotasks (setTimeout)

Dlaczego setTimeout 0 Nie Jest Natychmiastowy

console.log('Start');

setTimeout(() => {
    console.log('Timeout');
}, 0);

console.log('End');

// Wynik:
// Start
// End
// Timeout

Nawet z czasem 0, callback setTimeout trafia do kolejki macrotasks i czeka, aż cały kod synchroniczny się wykona. To fundamentalna zasada event loop - nigdy nie przerywamy wykonywania synchronicznego kodu.

3. Callback, Promise, Async/Await - Ewolucja Asynchroniczności

Na rozmowach często padają pytania o różnice między tymi trzema podejściami i kiedy którego używać.

Odpowiedź w 30 sekund

Callbacki to pierwotny sposób obsługi asynchroniczności - funkcja przekazywana jako argument, wywoływana po zakończeniu operacji. Problem: callback hell przy zagnieżdżaniu. Promise to obiekt reprezentujący przyszły wynik - może być pending, fulfilled lub rejected. Umożliwia łańcuchowanie przez .then(). Async/await to składnia na Promise - pozwala pisać kod asynchroniczny jak synchroniczny, z try/catch do obsługi błędów.

Callback Hell - Problem

// Tak wygląda "callback hell" - piramida zagłady
fs.readFile('config.json', (err, config) => {
    if (err) return handleError(err);

    db.connect(config, (err, connection) => {
        if (err) return handleError(err);

        connection.query('SELECT * FROM users', (err, users) => {
            if (err) return handleError(err);

            users.forEach(user => {
                sendEmail(user.email, (err, result) => {
                    if (err) return handleError(err);
                    console.log('Email wysłany do', user.email);
                });
            });
        });
    });
});

Problemy: trudne do czytania, obsługa błędów w każdym poziomie, ciężkie do debugowania.

Promise - Rozwiązanie

// Ten sam kod z Promise - płaski i czytelny
const fsPromises = require('fs').promises;

fsPromises.readFile('config.json')
    .then(config => db.connect(config))
    .then(connection => connection.query('SELECT * FROM users'))
    .then(users => {
        return Promise.all(
            users.map(user => sendEmail(user.email))
        );
    })
    .then(results => {
        console.log('Wszystkie emaile wysłane');
    })
    .catch(err => {
        // Jeden punkt obsługi wszystkich błędów
        console.error('Błąd:', err);
    });

Async/Await - Najczystsza Składnia

async function processUsers() {
    try {
        const config = await fsPromises.readFile('config.json');
        const connection = await db.connect(config);
        const users = await connection.query('SELECT * FROM users');

        // Równoległe wysyłanie emaili
        await Promise.all(
            users.map(user => sendEmail(user.email))
        );

        console.log('Wszystkie emaile wysłane');
    } catch (err) {
        console.error('Błąd:', err);
    }
}

Klasyczny Problem: Sekwencyjne vs Równoległe

// ZŁE - sekwencyjne, wolne
async function slow() {
    const user = await fetchUser(1);     // czeka 1s
    const posts = await fetchPosts(1);   // czeka kolejną 1s
    const comments = await fetchComments(1); // czeka kolejną 1s
    return { user, posts, comments };    // łącznie 3s
}

// DOBRE - równoległe, szybkie
async function fast() {
    const [user, posts, comments] = await Promise.all([
        fetchUser(1),
        fetchPosts(1),
        fetchComments(1)
    ]);
    return { user, posts, comments };    // łącznie 1s
}

Kandydaci, którzy od razu wspominają o Promise.all dla niezależnych operacji, robią bardzo dobre wrażenie.

4. Express.js i Middleware

Express to de facto standard dla API w Node.js. Rekruterzy sprawdzają, czy rozumiesz architekturę middleware.

Odpowiedź w 30 sekund

Middleware w Express to funkcje z dostępem do obiektów request, response i funkcji next(). Wykonują się w kolejności rejestracji, tworząc łańcuch. Każdy middleware może: zmodyfikować req/res, zakończyć żądanie przez res.send(), lub przekazać kontrolę dalej przez next(). Typowe zastosowania to logowanie, parsowanie body, uwierzytelnianie, obsługa błędów.

Anatomia Middleware

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

// Middleware ma dostęp do req, res i next
function loggerMiddleware(req, res, next) {
    console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
    next();  // Przekaż kontrolę do następnego middleware
}

// Middleware może modyfikować request
function authMiddleware(req, res, next) {
    const token = req.headers.authorization;

    if (!token) {
        return res.status(401).json({ error: 'Brak tokenu' });
        // Nie wywołujemy next() - łańcuch się kończy
    }

    try {
        const user = verifyToken(token);
        req.user = user;  // Dodajemy dane do request
        next();
    } catch (err) {
        res.status(401).json({ error: 'Nieprawidłowy token' });
    }
}

// Kolejność rejestracji MA znaczenie
app.use(loggerMiddleware);       // Każde żądanie
app.use(express.json());         // Parsowanie JSON body
app.use('/api', authMiddleware); // Tylko dla /api/*

app.get('/api/users', (req, res) => {
    // req.user jest dostępny dzięki authMiddleware
    res.json({ user: req.user });
});

app.listen(3000);

Middleware Obsługi Błędów

Middleware błędów ma specjalną sygnaturę - 4 argumenty:

// Zwykły middleware
app.use((req, res, next) => {
    // 3 argumenty
});

// Middleware błędów - MUSI mieć 4 argumenty
app.use((err, req, res, next) => {
    console.error('Błąd:', err.stack);

    // Nie ujawniaj szczegółów w produkcji
    const message = process.env.NODE_ENV === 'production'
        ? 'Błąd serwera'
        : err.message;

    res.status(err.status || 500).json({
        error: message
    });
});

// Jak przekazać błąd do error middleware
app.get('/api/data', async (req, res, next) => {
    try {
        const data = await fetchData();
        res.json(data);
    } catch (err) {
        next(err);  // Przekaż do error middleware
    }
});

Struktura Skalowalnej Aplikacji Express

/project
  /src
    /controllers
      userController.js
      postController.js
    /routes
      userRoutes.js
      postRoutes.js
      index.js
    /middlewares
      auth.js
      errorHandler.js
      validate.js
    /models
      User.js
      Post.js
    /services
      emailService.js
      paymentService.js
    /utils
      logger.js
    app.js
  /tests
  package.json
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const auth = require('../middlewares/auth');

router.get('/', userController.getAll);
router.get('/:id', userController.getById);
router.post('/', auth, userController.create);
router.put('/:id', auth, userController.update);
router.delete('/:id', auth, userController.delete);

module.exports = router;

// app.js
const userRoutes = require('./routes/userRoutes');
app.use('/api/users', userRoutes);

5. Streams - Przetwarzanie Dużych Danych

Streams to często pomijany temat, ale na rozmowach na stanowiska mid i senior pojawia się regularnie.

Odpowiedź w 30 sekund

Streams to abstrakcja do przetwarzania danych w kawałkach, bez wczytywania całości do pamięci. Są cztery typy: Readable (odczyt), Writable (zapis), Duplex (oba kierunki), Transform (modyfikacja podczas przepływu). Kluczowy mechanizm to backpressure - automatyczne wstrzymywanie producenta gdy konsument nie nadąża. Używamy ich przy dużych plikach, streamowaniu video, przetwarzaniu logów.

Problem Bez Streams

// ZŁE - wczytuje cały plik do pamięci
const fs = require('fs');

app.get('/video', (req, res) => {
    fs.readFile('film-2gb.mp4', (err, data) => {
        if (err) return res.status(500).send('Błąd');
        res.send(data);
    });
    // Przy 100 użytkownikach = 200GB w pamięci!
});

Rozwiązanie ze Streams

// DOBRE - streamuje plik kawałek po kawałku
const fs = require('fs');

app.get('/video', (req, res) => {
    const stream = fs.createReadStream('film-2gb.mp4');

    // Automatycznie obsługuje backpressure
    stream.pipe(res);

    stream.on('error', (err) => {
        console.error('Błąd streamu:', err);
        res.status(500).send('Błąd');
    });
});
// Zużywa tylko ~64KB pamięci niezależnie od rozmiaru pliku

Transform Stream - Przetwarzanie w Locie

const { Transform } = require('stream');
const fs = require('fs');

// Transform stream do konwersji na uppercase
const upperCaseTransform = new Transform({
    transform(chunk, encoding, callback) {
        // chunk to kawałek danych (Buffer)
        const upperCased = chunk.toString().toUpperCase();
        callback(null, upperCased);
    }
});

// Czytaj plik -> przekształć -> zapisz
fs.createReadStream('input.txt')
    .pipe(upperCaseTransform)
    .pipe(fs.createWriteStream('output.txt'))
    .on('finish', () => console.log('Gotowe!'));

Backpressure - Kontrola Przepływu

const fs = require('fs');

const readable = fs.createReadStream('duzy-plik.txt');
const writable = fs.createWriteStream('kopia.txt');

readable.on('data', (chunk) => {
    // write() zwraca false gdy bufor jest pełny
    const canContinue = writable.write(chunk);

    if (!canContinue) {
        // Wstrzymaj czytanie - konsument nie nadąża
        readable.pause();

        // Wznów gdy bufor się opróżni
        writable.once('drain', () => {
            readable.resume();
        });
    }
});

// Lub prościej - pipe() obsługuje to automatycznie
fs.createReadStream('duzy-plik.txt')
    .pipe(fs.createWriteStream('kopia.txt'));

6. Bezpieczeństwo Aplikacji Node.js

Na stanowiskach mid i senior pytania o bezpieczeństwo są standardem. Rekruterzy sprawdzają świadomość zagrożeń.

Odpowiedź w 30 sekund

Kluczowe zagrożenia dla Node.js to: injection (SQL, NoSQL, command), XSS przy renderowaniu HTML, CSRF przy formularzach, denial of service przez blokowanie event loop. Zabezpieczenia: walidacja i sanityzacja danych wejściowych, Helmet.js dla nagłówków HTTP, tokeny CSRF, rate limiting, parametryzowane zapytania, regularne audyty zależności przez npm audit.

SQL/NoSQL Injection

// ZŁE - podatne na injection
app.get('/user', async (req, res) => {
    const { email } = req.query;
    // Atakujący może wysłać: email=' OR '1'='1
    const query = `SELECT * FROM users WHERE email = '${email}'`;
    const result = await db.query(query);
    res.json(result);
});

// DOBRE - parametryzowane zapytanie
app.get('/user', async (req, res) => {
    const { email } = req.query;
    // Parametr jest automatycznie escapowany
    const query = 'SELECT * FROM users WHERE email = $1';
    const result = await db.query(query, [email]);
    res.json(result);
});

// Mongoose - też bezpieczne
const user = await User.findOne({ email: req.query.email });

Helmet.js - Nagłówki Bezpieczeństwa

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

// Helmet ustawia wiele nagłówków bezpieczeństwa
app.use(helmet());

// Lub selektywnie
app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
    }
}));

app.use(helmet.xssFilter());           // X-XSS-Protection
app.use(helmet.noSniff());             // X-Content-Type-Options
app.use(helmet.frameguard());          // X-Frame-Options
app.use(helmet.hsts());                // Strict-Transport-Security

Rate Limiting

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

// Ogólny limit
const generalLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,  // 15 minut
    max: 100,                   // max 100 żądań na IP
    message: { error: 'Zbyt wiele żądań, spróbuj później' }
});

// Strict limit dla logowania
const loginLimiter = rateLimit({
    windowMs: 60 * 60 * 1000,  // 1 godzina
    max: 5,                     // max 5 prób
    message: { error: 'Zbyt wiele prób logowania' }
});

app.use(generalLimiter);
app.use('/api/login', loginLimiter);

Walidacja Danych Wejściowych

const Joi = require('joi');

// Definiuj schemat walidacji
const userSchema = Joi.object({
    email: Joi.string().email().required(),
    password: Joi.string().min(8).max(128).required(),
    age: Joi.number().integer().min(18).max(120)
});

// Middleware walidacji
function validateBody(schema) {
    return (req, res, next) => {
        const { error, value } = schema.validate(req.body);

        if (error) {
            return res.status(400).json({
                error: 'Błąd walidacji',
                details: error.details.map(d => d.message)
            });
        }

        req.body = value;  // Używaj zwalidowanych danych
        next();
    };
}

app.post('/api/users', validateBody(userSchema), async (req, res) => {
    // req.body jest zwalidowane i bezpieczne
    const user = await User.create(req.body);
    res.status(201).json(user);
});

7. Testowanie Aplikacji Node.js

Pytania o testowanie pojawiają się na prawie każdej rozmowie. Rekruterzy chcą wiedzieć, czy piszesz testy i jak.

Odpowiedź w 30 sekund

Do testowania Node.js używam Jest lub Mocha z Chai. Testy jednostkowe sprawdzają pojedyncze funkcje w izolacji, mockując zależności przez Sinon lub jest.mock(). Testy integracyjne sprawdzają współpracę modułów, np. API z bazą. Do testów HTTP używam Supertest. Testy E2E sprawdzają pełny flow aplikacji.

Struktura Testu Jednostkowego

// userService.js
class UserService {
    constructor(userRepository, emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }

    async createUser(userData) {
        const user = await this.userRepository.create(userData);
        await this.emailService.sendWelcome(user.email);
        return user;
    }
}

// userService.test.js
const { jest } = require('@jest/globals');
const UserService = require('./userService');

describe('UserService', () => {
    let userService;
    let mockUserRepository;
    let mockEmailService;

    beforeEach(() => {
        // Mockujemy zależności
        mockUserRepository = {
            create: jest.fn()
        };
        mockEmailService = {
            sendWelcome: jest.fn()
        };

        userService = new UserService(mockUserRepository, mockEmailService);
    });

    describe('createUser', () => {
        it('powinno utworzyć użytkownika i wysłać email powitalny', async () => {
            // Arrange
            const userData = { email: 'test@example.com', name: 'Test' };
            const createdUser = { id: 1, ...userData };
            mockUserRepository.create.mockResolvedValue(createdUser);
            mockEmailService.sendWelcome.mockResolvedValue(true);

            // Act
            const result = await userService.createUser(userData);

            // Assert
            expect(mockUserRepository.create).toHaveBeenCalledWith(userData);
            expect(mockEmailService.sendWelcome).toHaveBeenCalledWith('test@example.com');
            expect(result).toEqual(createdUser);
        });

        it('powinno rzucić błąd gdy zapis się nie powiedzie', async () => {
            // Arrange
            mockUserRepository.create.mockRejectedValue(new Error('DB Error'));

            // Act & Assert
            await expect(userService.createUser({}))
                .rejects.toThrow('DB Error');
        });
    });
});

Testy Integracyjne z Supertest

const request = require('supertest');
const app = require('./app');
const db = require('./db');

describe('User API', () => {
    beforeAll(async () => {
        await db.connect();
    });

    afterAll(async () => {
        await db.disconnect();
    });

    beforeEach(async () => {
        await db.clear();
    });

    describe('POST /api/users', () => {
        it('powinno utworzyć nowego użytkownika', async () => {
            const response = await request(app)
                .post('/api/users')
                .send({
                    email: 'test@example.com',
                    password: 'haslo123',
                    name: 'Test User'
                })
                .expect('Content-Type', /json/)
                .expect(201);

            expect(response.body).toHaveProperty('id');
            expect(response.body.email).toBe('test@example.com');
            expect(response.body).not.toHaveProperty('password');
        });

        it('powinno zwrócić 400 dla nieprawidłowego emaila', async () => {
            const response = await request(app)
                .post('/api/users')
                .send({
                    email: 'nie-email',
                    password: 'haslo123'
                })
                .expect(400);

            expect(response.body).toHaveProperty('error');
        });
    });
});

8. Skalowanie i Wydajność

Na stanowiskach senior pytania o skalowanie są obowiązkowe. Rekruterzy sprawdzają doświadczenie z produkcyjnymi systemami.

Odpowiedź w 30 sekund

Node.js skalujemy przez: klastrowanie (moduł cluster lub PM2) do wykorzystania wielu rdzeni CPU, load balancing przez Nginx lub AWS ELB, cache (Redis) dla częstych danych, connection pooling do baz danych. Monitoruję wydajność przez APM (New Relic, Datadog) i profiluję przez Chrome DevTools z flagą --inspect.

Klastrowanie z Modułem Cluster

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    console.log(`Master ${process.pid} uruchomiony`);

    // Fork workers - jeden na każdy rdzeń CPU
    for (let i = 0; i < numCPUs; i++) {
        cluster.fork();
    }

    cluster.on('exit', (worker, code, signal) => {
        console.log(`Worker ${worker.process.pid} zakończył pracę`);
        // Automatyczny restart
        cluster.fork();
    });
} else {
    // Workers współdzielą port
    const app = require('./app');
    app.listen(3000);
    console.log(`Worker ${process.pid} uruchomiony`);
}

PM2 - Prostsze Klastrowanie

# Uruchom z automatycznym klastrowaniem
pm2 start app.js -i max  # max = liczba rdzeni CPU

# Lub w konfiguracji ecosystem.config.js
module.exports = {
  apps: [{
    name: 'api',
    script: './app.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production'
    }
  }]
};

Cache z Redis

const Redis = require('ioredis');
const redis = new Redis();

// Middleware cache'ujący
function cacheMiddleware(duration) {
    return async (req, res, next) => {
        const key = `cache:${req.originalUrl}`;

        try {
            const cached = await redis.get(key);

            if (cached) {
                return res.json(JSON.parse(cached));
            }

            // Przechwytujemy res.json aby zapisać do cache
            const originalJson = res.json.bind(res);
            res.json = async (data) => {
                await redis.setex(key, duration, JSON.stringify(data));
                return originalJson(data);
            };

            next();
        } catch (err) {
            next();  // Przy błędzie cache - kontynuuj bez cache
        }
    };
}

// Użycie
app.get('/api/products', cacheMiddleware(300), async (req, res) => {
    const products = await Product.find();
    res.json(products);
});

Na Co Rekruterzy Naprawdę Zwracają Uwagę

Po przeprowadzeniu wielu rozmów rekrutacyjnych na stanowiska Node.js developer, mogę powiedzieć, że sprawdzam następujące rzeczy:

Czy kandydat rozumie event loop. To fundament Node.js. Jeśli ktoś nie potrafi wyjaśnić kolejności wykonania setTimeout vs Promise.then, to znak, że używa Node.js bez zrozumienia.

Czy kandydat myśli o wydajności. Wspomnienie o blokowaniu event loop, streamach zamiast wczytywania całych plików, Promise.all dla równoległych operacji - to pokazuje praktyczne doświadczenie.

Czy kandydat zna zagrożenia bezpieczeństwa. Na stanowiskach mid i senior pytania o injection, XSS, rate limiting są standardem. Brak świadomości tych zagrożeń to czerwona flaga.

Czy kandydat pisze testy. Pytam o podejście do testowania, ulubione narzędzia, różnicę między testami jednostkowymi a integracyjnymi. Brak odpowiedzi sugeruje brak doświadczenia produkcyjnego.

Czy kandydat zna ograniczenia Node.js. Każda technologia ma słabe strony. Kandydat, który wie kiedy NIE używać Node.js, pokazuje dojrzałość techniczną.

Praktyka na Koniec

Zanim pójdziesz na rozmowę, upewnij się, że potrafisz odpowiedzieć na te pytania:

  1. Napisz prostą implementację rate limitera bez zewnętrznych bibliotek.
  2. Wyjaśnij, dlaczego ten kod może być problemem:
app.get('/api/hash', (req, res) => {
    const hash = crypto.pbkdf2Sync(req.query.password, 'salt', 100000, 64, 'sha512');
    res.json({ hash: hash.toString('hex') });
});
  1. Jak zaimplementowałbyś graceful shutdown serwera Express?
  2. Jaka jest różnica między process.nextTick() a setImmediate()?
  3. Jak obsłużysz nieobsłużone rejection w Promise (unhandled rejection)?
  4. Napisz middleware, który loguje czas wykonania każdego żądania.

Jeśli potrafisz na nie odpowiedzieć bez wahania, jesteś gotowy na większość rozmów z Node.js.

Zobacz też


Chcesz Więcej Pytań z Node.js?

Ten artykuł to wierzchołek góry lodowej. Mamy ponad 100 pytań z Node.js z szczegółowymi odpowiedziami, przykładami kodu i wyjaśnieniami - od podstaw przez Express i bazy danych po microservices i deployment.

Sprawdź Pełny Zestaw Pytań Node.js →

Lub wypróbuj nasz darmowy podgląd pytań Node.js, żeby zobaczyć więcej pytań w tym stylu.


Napisane przez zespół Flipcards, na podstawie ponad 15 lat doświadczenia w branży IT i setek przeprowadzonych rozmów rekrutacyjnych w firmach takich jak BNY Mellon, UBS i wiodących firmach fintech.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.