W projektach JavaScript nie każdy pakiet ma taki sam status. Część bibliotek musi działać w przeglądarce albo na serwerze po wdrożeniu, a część jest potrzebna tylko do budowania, testów, lintowania czy typowania kodu. To właśnie robi zapis do devDependencies, czyli najbliższy sens temu, co wiele osób skrótowo nazywa npm save dev. W praktyce chodzi o to, by nie mieszać narzędzi dla programisty z zależnościami, bez których aplikacja nie ruszy na produkcji.
Najkrócej: zapis do devDependencies porządkuje zależności używane tylko podczas pracy nad projektem
- Do devDependencies trafiają pakiety do budowania, testów, lintowania i typowania, a nie biblioteki wymagane w runtime.
-
npm install -Dinpm install --save-devzapisują paczkę w sekcjidevDependencies. - npm aktualizuje przy tym także
package-lock.json, więc instalacje w zespole są bardziej przewidywalne. - Na produkcji często używa się
npm ci --omit=devalbo instalacji bez zależności deweloperskich. - Jeśli pakiet jest potrzebny po wdrożeniu, nie powinien trafiać do sekcji dla narzędzi developerskich.
- Najczęstszy błąd to wrzucanie do złej sekcji bundlera, lintera albo biblioteki runtime.
Czym jest zależność deweloperska i kiedy ma sens
Zależność deweloperska to pakiet, który pomaga tworzyć, sprawdzać lub przygotować kod, ale nie jest potrzebny do jego uruchomienia w produkcji. Dokładnie tak rozróżnia to dokumentacja npm: dependencies są dla aplikacji działającej po wdrożeniu, a devDependencies dla lokalnego developmentu i testów.
Najczęściej trafiają tam narzędzia, których użytkownik końcowy nigdy nie widzi, choć bez nich zespół nie pracowałby wygodnie:
- ESLint - sprawdza jakość i spójność kodu przed commitem.
- Prettier - formatuje pliki bez wpływu na działanie aplikacji.
- TypeScript - kompiluje i typuje kod, ale sam w sobie nie jest częścią runtime.
- Vite, Webpack, Rollup - przygotowują paczkę do wdrożenia.
- Vitest, Jest, Cypress - odpowiadają za testy i weryfikację zachowania.
- @types/* - dostarczają definicje typów, potrzebne głównie podczas pracy nad kodem.
Jeśli pakiet jest potrzebny po stronie runtime, nie powinien lądować w tej kategorii. Przykładowo biblioteka do komunikacji z API w frontendzie albo sterownik bazy w backendzie zwykle zostaje w zwykłych zależnościach, bo bez niej aplikacja nie wykona podstawowej logiki. Z tego rozróżnienia wynika też to, jak powinien wyglądać kolejny krok instalacji.

Jak zapisać pakiet jako devDependency w praktyce
Najprostsza forma to npm install -D albo równoważne npm install --save-dev . npm dopisze wpis do package.json w sekcji devDependencies i zaktualizuje package-lock.json, dzięki czemu reszta zespołu dostanie ten sam zakres wersji.
npm install -D eslint
npm install --save-dev vitest
npm install -D @types/nodePo takiej instalacji w package.json zobaczysz wpis podobny do "eslint": "^9.0.0". Domyślnie npm zapisuje wersję z zakresem semver, zwykle z ^. Jeśli zależy ci na dokładnym przypięciu wersji, dodaj -E (--save-exact), ale robię to tylko wtedy, gdy naprawdę chcę zamrozić narzędzie, a nie tylko jego główną linię rozwojową.
W drugą stronę działa to symetrycznie: gdy pakiet przestaje być potrzebny, usuwam go przez npm uninstall -D . To ważne, bo porządek w zależnościach zaczyna się nie od samej instalacji, ale od konsekwentnego sprzątania po zmianach.
Jak odróżnić dependencies, devDependencies i inne typy
Najwięcej pomyłek bierze się stąd, że patrzy się tylko na nazwę pakietu, a nie na moment jego użycia. Ja zadaję jedno pytanie: czy kod bez tego pakietu uruchomi się po wdrożeniu?
| Typ | Kiedy używać | Przykłady | Co się dzieje w produkcji |
|---|---|---|---|
dependencies |
Gdy pakiet jest potrzebny w runtime | React w aplikacji, klient HTTP, biblioteka dostępu do bazy | Musi być dostępny, bo aplikacja z niego korzysta |
devDependencies |
Gdy pakiet służy do budowania, testów, lintowania lub typowania | ESLint, Prettier, Vitest, TypeScript, Vite | Często jest pomijany w instalacji produkcyjnej |
peerDependencies |
Gdy tworzysz bibliotekę i wymagasz konkretnej wersji pakietu hosta | Plugin do Reacta, rozszerzenie do frameworka | Oczekuje, że host dostarczy ten pakiet sam |
optionalDependencies |
Gdy pakiet jest dodatkiem, a jego brak nie powinien wywalić instalacji | Natywne dodatki, niekrytyczne rozszerzenia | Instalacja może pominąć taki pakiet bez blokowania projektu |
Warto też pamiętać, że peerDependencies służą do współpracy z pakietem hosta, a nie do „schowania” czegoś z produkcji. To częsty błąd przy tworzeniu bibliotek UI i pluginów. Gdy nie jestem pewien, sprawdzam, czy pakiet ma uruchamiać aplikację, czy tylko pomóc mi ją zbudować albo przetestować.
Co dzieje się przy instalacji lokalnie, w CI i na produkcji
Sam zapis do devDependencies nie znaczy jeszcze, że pakiet zniknie z dysku. Przy zwykłym npm install npm instaluje także zależności deweloperskie, o ile nie ustawisz pomijania dla środowiska produkcyjnego. W praktyce najczęściej spotkasz npm install --omit=dev albo NODE_ENV=production, które prowadzą do pominięcia pakietów z tej sekcji.
-
npm install- pełna instalacja do pracy lokalnej. -
npm ci- czysta, powtarzalna instalacja na CI, oparta na lockfile. -
npm ci --omit=dev- wariant dla produkcji, gdy nie chcesz instalować narzędzi deweloperskich.
To ważne, bo w wielu deploymentach właśnie devDependencies odpowiadają za rozmiar instalacji i czas uruchamiania pipeline'u. Jeśli projekt trafia do środowiska produkcyjnego bez bundlowania, pominięcie tej sekcji potrafi realnie zmniejszyć obraz kontenera albo paczkę wdrożeniową. Z drugiej strony w klasycznym buildzie front-endowym te pakiety bywają niezbędne na etapie kompilacji, więc nie wolno ich usuwać „na ślepo”.
Najczęstsze błędy, które psują porządek w projekcie
-
Wrzucone do złej sekcji narzędzie buildowe. Jeśli Vite, ESLint albo TypeScript lądują w
dependencies, produkcja niesie zbędny balast. -
Runtime bez zależności produkcyjnej. Jeśli aplikacja po wdrożeniu nie znajdzie biblioteki, która była w
devDependencies, błąd wyjdzie dopiero na serwerze. -
Brak lockfile w repozytorium. Bez
package-lock.jsonzespół może instalować inne zakresy wersji niż ty. - Globalna instalacja zamiast lokalnej. Narzędzie dostępne tylko na twoim komputerze nie rozwiązuje problemu w projekcie.
- Za szybkie kasowanie zależności deweloperskiej. Jeśli pakiet bierze udział w buildzie, jego usunięcie rozwala pipeline albo testy.
Najlepsza obrona przed takimi wpadkami jest prosta: przed instalacją sprawdzam, czy pakiet jest potrzebny do uruchomienia aplikacji, czy tylko do jej przygotowania. To jedno pytanie oszczędza więcej czasu niż szukanie późniejszych błędów w CI. Gdy trzeba coś zmienić, robię to lokalnie, zapisuję w odpowiedniej sekcji i od razu weryfikuję, czy build nadal przechodzi.
Praktyczna reguła, która porządkuje większość decyzji
Gdybym miał to uprościć do jednego zdania, powiedziałbym: jeśli pakiet nie jest potrzebny do działania aplikacji po wdrożeniu, zapisuję go jako zależność deweloperską. Dzięki temu plik zależności pozostaje czytelny, build jest lżejszy, a osoba wchodząca do projektu od razu widzi, co służy runtime, a co tylko pracy zespołu.
W projektach front-endowych ta zasada najczęściej oznacza taki podział: React, router, klient API czy biblioteka do komunikacji z backendem trafiają do zwykłych zależności, natomiast bundler, linter, test runner, TypeScript i typy trafiają do devDependencies. To nie jest tylko porządek estetyczny. W większym zespole taki podział ogranicza błędy w deploymentach i ułatwia automatyzację.
Jeśli chcesz utrzymać projekt w ryzach, trzymaj jeszcze jedną zasadę pomocniczą: instaluj lokalnie, zapisuj świadomie, a na CI używaj npm ci. Taki zestaw daje przewidywalne instalacje i nie zmusza cię do zgadywania, czy problem wynika z kodu, czy z rozjechanych zależności.