Strona główna / Artykuły

Projekt systemu backendowego dla zespołów React: GraphQL, buforowanie, kolejki i wdrażanie

Pięć błędów w warstwie backendowej, które popełniają zespoły React skupione na frontendzie, od wyboru między REST a GraphQL po katalogowanie, kolejki wiadomości, logi i procesy wdrażania, wraz z rozwiązaniami związanymi z projektowaniem systemu.

Wydawca: REACTAPP.TOP14 min czytania3258 słów

Źródła: 5 Deep Technical Backend Mistakes React Devs Make in Production (And How to Fix Them)

Jako deweloperzy React możecie z łatwością tworzyć nowoczesne, responsywne interfejsy. Znajomo macie równoczesne renderowanie, komponenty serwerowe oraz złożone wzorce zarządzania stanem. Jednak gdy rozmowa przechodzi na systemy backendowe, wielu inżynierów skupionych na stronie frontend nadal pracuje na poziomie projektów hobbystycznych. Podstawowe middleware Express, bezpośrednie zapytania do MongoDB oraz platformy deployment, które ukrywają infrastrukturę, stanowią powszechne standardy. Wszystko działa płynnie na localhost, a w środowisku testowym również wygląda dobrze. Ale gdy pojawia się rzeczywisty ruch produkcyjny, słabości szybko się ujawniają.

Backend to nie tylko usługa, która przekazuje JSON do frontendu. To system odpowiedzialny za zarządzanie równoczesnością, przywracanie po awariach, opóźnieniami oraz skalowalnością. Ten tekst analizuje pięć błędów w backendzie, które często pojawiają się w projektach opartych na React po wejściu w produkcję, wraz ze zmianami architektonicznymi potrzebnymi do ich naprawy.

Błąd 1: poleganie na middleware’u Express bez zrozumienia REST, GraphQL i gRPC

Znajomym rozwiązaniem dla deweloperów React jest aplikacja Express połączona z serią wywołań app.use(). Konfiguruje się w niej cors, body-parser, morgan, dodaje się kilka obsługowników tras oraz zwraca się dane w formacie JSON. Jest to wygodne rozwiązanie, ponieważ odzwierciedla te same wzorce JavaScriptu, których już używa się w części frontendowej. Problem polega na tym, że taki podejście pomija ważniejsze pytanie: jaki paradygmat API faktycznie pasuje do przekazywanych danych?

Układanie warstw middleware bez uprzedniego rozważenia struktury umowy danych sprawia, że powstają sztywne punkty końcowe, które wysyłają zbyt duże obciążenia w formacie JSON do klientów mobilnych. W rezultacie otrzymujemy coś w rodzaju /api/user/123, które zwraca rekord użytkownika wraz z jego zamówieniami, adresami, preferencjami i historią działań – wszystko dlatego, że kiedyś jeden ekran potrzebował pełnego obrazu sytuacji. Od tego momentu każdy użytkownik tego punktu końcowego płaci cenę tego pojedynczego przypadku użycia.

Rozwiązanie: świadome wybieranie paradygmatu API

Kluczowe jest zrozumienie zalet i wad REST, GraphQL i gRPC oraz świadome wybieranie jednego z nich dla każdego przypadku użycia.

REST jest prosty w użyciu i dobrze funkcjonuje z buforowaniem, ale narażony jest na nadmierną pobieranie danych. To, co zwraca punkt końcowy, trafia do komponentu React, nawet jeśli potrzebne są tylko kilka pól. Przy wolniejszych połączeniach mobilnych ten dodatkowy ciężar danych powoduje wolniejsze renderowanie i może odstraszyć użytkowników.

GraphQL rozwiązuje problem nadmiernego pobierania danych, pozwalając klientowi określić dokładnie te pola, które chce otrzymać. Za to występuje problem po stronie serwera, znany jako problem z zapytaniami N+1. Załóżmy, że mechanizm rozwiązywania zapytań pobiera listę użytkowników, a następnie wysyła oddzielne zapytanie dla każdego z nich w celu pobraania ich zamówień – jedno wejściowe zapytanie zamienia się w setki ruchów do bazy danych. Bez narzędzi takich jak DataLoader lub grupowanie zapytań na poziomie pól, serwer GraphQL nie poradzi sobie z rzeczywistym ruchem.

gRPC opiera się na Protocol Buffers zamiast na JSON, co zapewnia binarne dane o wielkości około dziesięciu razy mniejszej i znacznie szybsze do parsowania. Nie jest przeznaczony do API dostępnych z przeglądarki – przeglądarki nie mogą natywnie obsługiwać treści HTTP/2 bez proxy’u pomiędzy nimi – ale doskonale nadaje się do wewnętrznej komunikacji między usługami. Gdy brama Node.js musi komunikować się na przykład z usługą analityczną opartą na Pythonie lub usługą autoryzacji opartą na Go, gRPC z protobuf znacznie przewyższa REST-over-JSON pod względem efektywności w sieci wewnętrznej.

Wybór REST lub GraphQL w zależności od przypadku użycia

Dla API dostępnego publicznie, które zasila interfejs frontend zbudowany na React, zazwyczaj najlepiej sprawdza się hybrydowa strategia. Korzystaj z REST do prostych operacji CRUD, gdzie ważne jest cacheowanie. Użyj GraphQL, gdy wymagania dotyczące danych są głęboko zagnieżdżone i złożone, ale połącz go z DataLoader, aby wywołania bazy danych były grupowane i unikalizowane. Zachowaj gRPC do komunikacji wewnętrznej między usługami znajdującymi się za twoim gatewayem.

Poniżej znajduje się przykład rozwiązania GraphQL, które grupuje żądania za pomocą DataLoader:

// userLoader.js
const DataLoader = require('dataloader');
const batchUsers = async (ids) => {
  // Single query for all IDs
  const users = await db.user.findMany({
    where: { id: { in: ids } }
  });

  // Return in the same order as the keys
  const userMap = new Map(users.map(u => [u.id, u]));
  return ids.map(id => userMap.get(id) || null);
};
const userLoader = new DataLoader(batchUsers);
// Resolver
const resolvers = {
  Order: {
    user: (parent) => userLoader.load(parent.userId),
  }
};

Pominięcie tego narzędzia oznacza, że obsługa stu zamówień wymaga wysłania stu oddzielnych zapytań SELECT. Jeśli je dodamy, wszystkie one łączą się w jedno zapytanie SELECT ... WHERE id IN (...). To właśnie różnica pomiędzy odpowiedzią w 50 ms a żądaniem, które timeoutuje po trzech sekundach.

Błąd 2: bezpośrednie korzystanie z bazy danych przy każdym odczycie

Gdy każde odświeżenie strony wywołuje nowe zapytanie do bazy danych, to, co mamy, to w rzeczywistości nie architektura, lecz zwykła ścieżka przekazywania danych. Bazy danych takie jak MongoDB i PostgreSQL są szybkie, ale nie bezgranicznie. Przy jednoczesnym obciążeniu zasoby połączeń się wyczerpują, zapytania gromadzą się w kolejce, a czasy odpowiedzi, które kiedyś wynosiły 20 milisekund, mogą wzrosnąć do 5 sekund.

Często zdarza się, że deweloperzy React traktują bazę danych jak zwykły obiekt JavaScript w pamięci. Zapytanie Mongoose lub wywołanie Prisma trafia bezpośrednio do obsługi trasy, wynik jest zwracany i to już koniec. To rozwiązanie działa dobrze, dopóki produkt nie zaczyna przyciągać prawdziwych użytkowników. Wtedy zużycie CPU bazy danych osiąga maksimum, wykres opóźnień API przypomina stromy urwisko, a użytkownicy patrzą na ikony ładowania, które nigdy się nie kończą.

Rozwiązanie: warstwa cache Redis, której rozumiesz

Rozwiązaniem jest dodanie warstwy cache oraz rzeczywiste zrozumienie jej działania, zamiast bezrefleksyjnego jej wdrożenia. Redis to nie po prostu „szybka baza danych” – traktuj go jako bufor umieszczony strategicznie pomiędzy twoją aplikacją a magazynem danych, który faktycznie przechowuje dane.

Zacznij od wzorca Cache-Aside. Gdy przychodzi żądanie, najpierw sprawdź je w Redis. Jeśli wartość istnieje i nie wygasła, od razu ją zwróć, bez żadnych wyjść do bazy danych. Jeśli jej brakuje, mamy do czynienia z nieudanym dostępem z pamięci cache: zapytaj główną bazę danych, zapisz wynik do Redis z ustawionym czasem trwania i dopiero wtedy go zwróć. Każde kolejne żądanie tych samych danych zostanie obsłużone z pamięci w mniej niż milisekundę.

Przy prawidłowym zastosowaniu ten wzorzec może zmniejszyć obciążenie bazy danych o aż 90 procent w przypadku obciążeń skupionych na odczytach. Problem polega na tym, że wymaga dyscypliny: musisz unieważniać lub odświeżać wpisy z pamięci cache za każdym razem, gdy zmienia się odpowiadająca im rekord, w przeciwnym razie twoi użytkownicy będą mieć dostęp do przestarzałych danych.

Oto jak ten wzorzec wygląda w kodzie:

// cache.js
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function getCachedOrFetch(key, fetchFn, ttlSeconds = 300) {
  // 1. Check cache
  const cached = await redis.get(key);
  if (cached) {
    return JSON.parse(cached);
  }

  // 2. Cache miss: fetch from database
  const data = await fetchFn();

  // 3. Store in Redis with TTL
  await redis.setex(key, ttlSeconds, JSON.stringify(data));

  return data;
}
// Route handler
app.get('/api/products/:id', async (req, res) => {
  const { id } = req.params;

  const product = await getCachedOrFetch(
    `product:${id}`,
    () => db.product.findById(id), // Only runs on cache miss
    600 // 10 minutes
  );

  if (!product) return res.status(404).json({ error: 'Not found' });
  res.json(product);
});

Oto krok unieważniania, który jest wykonywany za każdym razem, gdy zapis produktu zostaje zaktualizowany:

async function updateProduct(id, updates) {
  const updated = await db.product.update(id, updates);
  await redis.del(`product:${id}`); // Invalidate
  return updated;
}

Istnieje również decyzja projektowa, którą warto podjąć świadomie: trzeba wiedzieć, kiedy PostgreSQL jest lepszym wyborem niż MongoDB. Jeśli twoja strona frontend z Reactem musi renderować panele kontrolne zawierające łączenia tabel, agregacje oraz analizy serii czasowych, dobrze zoptymalizowana instancja PostgreSQL zawsze przewyższy MongoDB. MongoDB jest dobrym wyborem dla danych w formie dokumentów, które nie mają wielu powiązań. PostgreSQL okazuje się lepszym rozwiązaniem, gdy dane mają rzeczywistą strukturę, a zapytania opierają się na JOIN.

Błąd 3: budowanie monolitów synchronicznych

Załóżmy, że użytkownik przesyła zdjęcie o wysokiej rozdzielczości przez twoją aplikację React. Twój serwer Express bierze plik, zmienia jego rozmiar na pięć różnych wartości, kompresuje każdą wersję, wysyła je wszystkie do S3, aktualizuje wiersz w bazie danych, a dopiero po zakończeniu tych działań wysyła odpowiedź 200 OK. Użytkownik musi wtedy czekać dwanaście sekund na ikonkę spinera. A jeśli krok zmiany rozmiaru zawiedzie w trakcie, cała prośba zostaje przerwana i użytkownik musi ponownie przesłać plik.

To właśnie jest przykład synchronicznego monolitu w działaniu: wątek obsługujący prośbę pozostaje zablokowany, dopóki nie zostaną ukończone wszystkie kroki. Gdy ruch wzrośnie, serwer zabraknie dostępnych wątków, kolejka oczekujących odpowiedzi się wydłuży, a cała aplikacja zacznie wydawać się zamarznięta.

Rozwiązanie: praca oparta na zdarzeniach z użyciem kolejk wiadomości

W tym przypadku potrzebna jest architektura oparta na zdarzeniach, zbudowana wokół kolejków wiadomości. Nie ma powodu, aby frontend czekał na zadania, które nie muszą zostać wykonane przed wysłaniem odpowiedzi.

Gdy tylko obraz dotrze, backend powinien zapisać surowy plik do tymczasowego przechowywania, umieścić wiadomość w kolejce i natychmiast odpowiedzieć kodem 202 Accepted wraz z identyfikatorem zadania. Frontend otrzymuje ten kod 202 natychmiast, a następnie albo sprawdza stan zadania, albo subskrybuje się przez WebSocket, aby dowiedzieć się, gdy zadanie zostanie zakończone. Oddzielnie dedykowana usługa pracownicza pobiera wiadomość z kolejki, wykonywa faktyczną pracę i aktualizuje bazę danych po jej zakończeniu.

Jeśli pracownik zakończy pracę w trakcie wykonywania zadania, kolejka automatycznie spróbuje ją ponownie. Gdy kolejka zaczyna się zapełniać, wystarczy dodać więcej pracowników, niezależnie od serwerów API. Rezultatem jest szybka praca frontendu oraz odporność backendu pod presją.

Oto ten sam proces zaimplementowany przy użyciu BullMQ i Redis:

// api.js — The HTTP layer
const { Queue } = require('bullmq');
const imageQueue = new Queue('image-processing', { connection: redis });
app.post('/api/upload', upload.single('image'), async (req, res) => {
  const job = await imageQueue.add('process-image', {
    filePath: req.file.path,
    userId: req.user.id,
  }, {
    attempts: 3,
    backoff: { type: 'exponential', delay: 2000 }
  });

  // Return immediately. Work happens elsewhere.
  res.status(202).json({ jobId: job.id, status: 'processing' });
});
// worker.js - The background processor
const { Worker } = require('bullmq');
const sharp = require('sharp');
const imageWorker = new Worker('image-processing', async (job) => {
  const { filePath, userId } = job.data;

  // Heavy work happens here, not in the API thread
  const sizes = [1200, 800, 400, 200];
  const uploads = sizes.map(async (size) => {
    const buffer = await sharp(filePath)
      .resize(size)
      .jpeg({ quality: 85 })
      .toBuffer();

    return s3.upload({
      Bucket: 'my-bucket',
      Key: `users/${userId}/image-${size}.jpg`,
      Body: buffer,
    }).promise();
  });

  await Promise.all(uploads);
  await db.user.update(userId, { imageProcessed: true });

  // Notify frontend via WebSocket or push notification
  await notifyUser(userId, { type: 'IMAGE_READY' });

}, { connection: redis, concurrency: 5 });

Jedynym zadaniem warstwy API jest obsługa protokołu HTTP; jedynym zadaniem warstwy roboczej jest zużywanie mocy obliczeniowej CPU. Skałują się one na oddzielnych osiach. Dziesięć tysięcy przesyłań może oznaczać uruchomienie dziesięciu instancji API wraz z pięćdziesięcioma procesorami roboczymi. To właśnie taka separacja stanowi prawdziwą architekturę.

Błąd 4: zakładanie, że backend to po prostu więcej JavaScript

Gdy aplikacja React wyświetla błąd 500, naturalną reakcją jest jego przechwycenie, pokazanie komunikatu informacyjnego i przekazanie problemu osobie odpowiedzialnej za backend. Jednak w większości rzeczywistych systemów frontend i backend nie są czysto oddzielone pod względem języka programowania. Brama API, z którą komunikujesz się, może być napisana w Node.js, podczas gdy podstawowa logika biznesowa znajduje się w usłudze Java, autoryzacja jest realizowana w Go, a silnik rekomendacji napisany jest w Pythonie.

Jeśli nie potrafisz przeanalizować śladu stosu w Javie ani zrozumieć sytuacji panic w Go, w praktyce debugujesz z zamkniętym okiem. Spędzisz godziny, czekając, aż ktoś inny powie ci, że prawdziwą przyczyną była wyczerpana baza połączeń do bazy danych – co sam mógłbyś odkryć w ciągu kilku minut, po prostu czytając logi.

Rozwiązanie: czytanie logów w różnych językach

Nauč się czytać logi z systemów napisanych w językach, którymi sam koniecznie nie programujesz. Nie musisz biegle znać składni Javy czy Go; wystarczy, że potrafisz rozpoznać ich typowe oznaki awarii.

Ślad stosu w Javie przedstawia historię od dołu do góry, przy czym rzeczywista przyczyna problemu znajduje się zazwyczaj blisko góry – np. w postaci NullPointerException, ConnectionPoolTimeoutException lub HeapSpaceError. Gdy zobaczysz komunikat Caused by: java.sql.SQLException: Connection pool exhausted, od razu wiesz, że baza danych jest przeładowana jednoczesnymi żądaniami. Rozwiązanie nie leży w warstwie Javy, lecz w konfiguracji puli połączeń lub w optymalizacji podstawowych zapytań.

Panicy w Go są zazwyczaj bardziej bezpośrednie – wskazują dokładną goroutine, plik i linię, w których doszło do błędu. Komunikat taki jak panic: runtime error: invalid memory address or nil pointer dereference oznacza, że jakaś struktura została użyta, zanim została zainicjowana.

Gdy próbujesz prześledzić problem od strony frontendu, oto na co należy zwrócić uwagę:

  • Wyczerpane połączenia bazy danych: przeszukaj logi wyszukiwania pod kątem haseł timeout, pool, connection refused lub too many clients. Zazwyczaj wskazuje to na konieczność lepszego zarządzania połączeniami w tle lub dodania replik do odczytu.
  • Wyczerpanie pamięci: szukaj wpisów HeapSpace, OOM lub Killed w logach kontenera. Rozwiązania zazwyczaj polegają na optymalizacji zapytań, dodaniu funkcji paginacji lub zwiększeniu limitów pamięci kontenera.
  • Błędy serializacji: zwracaj uwagę na komunikaty takie jak JSON parse error lub cannot serialize. Zazwyczaj oznaczają one, że struktura danych wysyłanych przez frontend już nie odpowiada temu, czego oczekuje backend.

Jeśli twoja organizacja korzysta z centralizowanej platformy logowania, takiej jak Datadog, Splunk lub zestaw ELK, poświęć czas na naukę prawidłowego przeprowadzania zapytań do niej. Porównaj datę i godzinę błędu na stronie klienta z wpisami z logów serwera backendu z tego samego momentu oraz śledź identyfikator żądania w miarę, jak przemieszcza się ono pomiędzy poszczególnymi usługami. Inżynier frontendu, który potrafi prześledzić całe żądanie przez cały zestaw usług, to ten, który ostatecznie wprowadza rzeczywiste rozwiązanie, zamiast jedynie składać zgłoszenie w tej sprawie.

Błąd 5: wdrażanie rozwiązania tylko dlatego, że działało poprawnie lokalnie

Platformy takie jak Heroku i Vercel przez lata ukrywały przed programistami problemy z infrastrukturą. Wystarczyło zapisać kod, a ten po prostu działał. To świetne do tworzenia prototypów i nauki podstaw, ale powoduje poważną lukę w zrozumieniu tego, jak faktycznie zachowują się systemy produkcyjne. Gdy coś się psuło, nie mieliśmy żadnego pojęcia o systemie operacyjnym, warstwie sieciowej ani o tym, jak zarządzane są kontenery, a odtworzenie błędu lokalnie było bezcelowe, ponieważ nasza lokalna konfiguracja w ogóle nie przypominała środowiska produkcyjnego.

Rozwiązanie: Docker, Kubernetes i podstawy CI/CD

Inwestuj czas w Docker, Kubernetes i CI/CD – nie na poziomie specjalisty od infrastruktury, ale na tyle, by móc myśleć jak architekt. Powinieneś wiedzieć, co dokładnie dzieje się z twoim kodem tuż po jego zapisaniu.

Wartość Dockera polega na możliwości odtworzenia wyników: plik Dockerfile precyzyjnie określa, jaki system operacyjny, zależności oraz środowisko wykonawcze są potrzebne, aby aplikacja działała poprawnie. Użycie budowy wieloetapowej sprawia, że ostateczny obraz produkcyjny jest lżejszy i bezpieczniejszy, ponieważ oddziela narzędzia potrzebne do budowy aplikacji od tych wymaganych do jej faktycznego uruchomienia.

Poniżej znajduje się przykład pliku Dockerfile wieloetapowego przeznaczonego do użycia w produkcji dla backendu opartego na Node.js:

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user for security
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
# Copy only necessary files from builder
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/main.js"]

Wynikowy obraz pozostaje poniżej 150 MB, ponieważ nie zawiera kompilatorów TypeScript, narzędzi do budowania oraz map źródłowych. Działa pod kontem użytkownika innym niż root i zawiera wyłącznie to, co jest rzeczywiście potrzebne do jego uruchomienia.

Kubernetes przejmuje zarządzanie tymi kontenerami na dużą skalę. Zasób Deployment określa, ile kopii Twojej API powinno być uruchomionych jednocześnie. Service zajmuje się równomiernym rozkładem ruchu między tymi kopiami. HorizontalPodAutoscaler automatycznie dodaje nowe pods, gdy zużycie CPU przekroczy około 70 procent, a następnie je usuwa, gdy popyt spadnie.

Oto, jak wygląda ta konfiguracja w praktyce:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-backend
  template:
    metadata:
      labels:
        app: api-backend
    spec:
      containers:
      - name: api
        image: my-registry/api:latest
        ports:
        - containerPort: 3000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-backend-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-backend
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Gdy ruch sieciowy rośnie, Kubernetes automatycznie uruchamia więcej kontenerów; gdy spada, wyłącza je ponownie. Twoja API nie ulega załamaniu pod obciążeniem – skaluje się, aby mu sprostać.

Celem pipeline’u CI/CD jest upewnienie się, że to, co sprawdziłeś przed połączeniem zmian, to dokładnie to, co będzie działać w środowisku produkcyjnym. Dobrze zaprojektowany pipeline obejmuje automatyczne testy na poziomie jednostek i integracji, skanowanie bezpieczeństwa, a dopiero potem tworzenie obrazu kontenera, wszystko to zanim cokolwiek zostanie wpuszczone w ruch sieciowy. Awaria na którymkolwiek z tych etapów natychmiast zatrzymuje wdrożenie – to właśnie ten mechanizm zapobiega temu, by uszkodzona zmiana dotarła do prawdziwych użytkowników.

Myślenie o projektowaniu systemu, a nie o składni

Przejście z roli dewelopera frontendu na inżyniera full-stack nie polega na opanowaniu nowej składni, ani nie jest jedynie kwestią pisania kodu w Node.js zamiast React. Chodzi o zrozumienie tego, jak dane faktycznie przepływają przez system, jak są cacheowane, jak są przetwarzane asynchronicznie oraz jak są wdrażane i skalowane.

Backend to nie jakiś nieprzejrzysty serwis, który po prostu zwraca JSON. To złożony system pełen ograniczeń, możliwych awarii oraz elementów, które mogą wpłynąć na wydajność. Gdy już zrozumiesz paradygmaty API, strategie cacheowania, kolejki wiadomości, debugowanie w różnych językach oraz orkiestrację kontenerów, przestaniesz tworzyć tylko dema, a zaczniesz budować systemy zdolne poradzić sobie z rzeczywistymi użytkownikami, ruchem i awariami.

Literatura pokrewna

  • React i JavaScript: Odpowiedzi na rozmowy rekrutacyjne, które pokazują prawdziwą głębię: Odkryj silniejsze, bardziej wyrafinowane odpowiedzi na częste pytania z rozmów rekrutacyjnych dotyczące React i JavaScript, od Virtual DOM po projektowanie systemów, które ukazują głębszą biegłość inżynierską.
  • REST vs GraphQL: Prawdziwe kompromisy stojące za każdą architekturą: Wyjaśnia konkretne problemy, które rozwiązują REST i GraphQL, ich wewnętrzną mechanikę oraz ukryte kompromisy, które należy wziąć pod uwagę przed wyborem jednego z nich do swojej API.
  • Zasady cachingu w Redisie: wzory, potknięcia i koncepcje z rozmów przesłuchaniowych: Dowiedz się, jak działa caching w Redisie w aplikacjach Node.js – od strategii typu cache-aside i TTL po mechanizmy ochrony przed przeciążeniem, zasady usuwania danych oraz typowe pytania z rozmów kwalifikacyjnych.
  • Projektowanie systemu backend według wąskich gardeł: od skracaczy URL do e-commerce: Podejście oparte na wymaganiach do projektowania backendów w Node.js – kiedy dodawać balansery obciążenia, Redis, repliki, kolejki i ograniczenia szybkości, oraz ile każde z tych elementów kosztuje.
  • Poza CRUD: Dziesięć praktyk architektonicznych, które pomagają utrzymać aplikacje MERN w stanie dostępnym: Poznaj praktyki na poziomie systemu, które zapewniają zdrowy rozwój aplikacji MERN: odpowiedzialność za dane, umowy API, stan pochodny, kod warstwowy, lekkie dane do przesyłania oraz spójne komunikaty o błędach.
  • Siedem pytań do odpowiedzi przed napisaniem pierwszej linii funkcji: Lista kontrolna przed implementacją obejmująca rzeczywisty problem użytkownika, gwarancje ukończenia pracy, odpowiedzialność za zasady, dane z systemów starszych, próby ponowne i współbieżność, możliwość obserwacji oraz bezpieczeństwo wdrożenia.
  • Tabele Outbox i Inbox: Projektowanie webhooki, które przetrwają błędy: Dowiedz się, jak transakcyjny outbox, idempotentny inbox, rozwiązania oparte na świadomości stanu oraz kolejki wiadomości martwych zapewniają niezawodną dostawę webhooki w AWS, Azure i GCP.
  • Zapytanie GraphQL, które wyczerpało zasoby bazy danych: Jedno zagnieżdżone dokumenty GraphQL wyczerpało zasoby bazy danych produkcyjnej. Dlaczego nie zadziałały limity przepustowości, timeouty HTTP oraz DataLoader, oraz jakie cztery rozwiązania ostatecznie to opanowały.