typescript and javascript logo
author avatar

Grzegorz Dubiel

27-03-2025

Implementacja uwierzytelniania w NestJS z Drizzle i Passport.js

NestJS to backendowy framework, który wyróżnia się na tle innych frameworków w ekosystemie Node.js. W przeciwieństwie do Expressa czy Hono, które dają pełną swobodę w organizacji kodu aplikacji, NestJS narzuca bardziej sztywne reguły. Pomimo podzielonych zdań deweloperów ze społeczności JavaScript, NestJS stale zyskuje na popularności, zajmując drugie miejsce w kategorii frameworków backendowych w ankiecie State of JS 2024.

Wysokopoziomowy przegląd kluczowych koncepcji NestJS

NestJS wymusza organizację kodu za pomocą dobrze zdefiniowanych bloków konstrukcyjnych, zazwyczaj podzielonych na moduły według funkcjonalności lub domeny, takich jak np. users, auth czy products. Każdy katalog modułu zazwyczaj składa się z co najmniej trzech kluczowych plików: service, controller i module. Serwis odpowiada za logikę biznesową, kontroler obsługuje zapytania, moduł określa granice oraz zasady, według których różne części funkcjonalności mogą ze sobą współdziałać.

Jedną z najważniejszych cech NestJS jest wstrzykiwanie zależności(dependency injection). Każda wielokrotnego użytku klasa, np. service, jest oznaczona jako możliwa do wstrzyknięcia za pomocą dekoratora @Injectable(). Dzięki temu system DI NestJS może wstrzyknąć ją do konstruktora innej klasy — na przykład service może zostać wstrzyknięty do controller.

typescript

Dekorator @Injectable() działa jako metadana, które system DI odczytuje w czasie działania aplikacji.

typescript

Magia, prawda? W naszym kontrolerze wystarczy zaimportować UserService, a NestJS wstrzyknie go do UserController, udostępniając go w konstruktorze.

Jednak aby wszystko działało poprawnie, musimy wykonać jeszcze jeden krok. Jak wspomniałem wcześniej, należy jawnie zdefiniować zasady i granice dla klas w katalogu users, tworząc plik users.module.ts.

typescript

Przegląd przykładowego projektu

Po zdobyciu ogólnego, wysokopoziomowego zrozumienia, jak działa NestJS, możemy teraz rozpocząć budowanie naszego małego przykładu uwierzytelniania.

Użyjemy Google jako identity provider'a (IdP), zarządzanie strategiami uwierzytelniania będzie realizowane przez passport.js, a dane użytkowników przechowywane w PostgreSQL, drizzle będzie służyć nam jako ORM.

Konfiguracja projektu

Zainstalujmy NestJS wraz z wymaganymi zależnościami.

terminal

Do uwierzytelniania za pomocą Google musimy przechować sekrety w pliku .env. Sekrety te można uzyskać z Google Developer Console.

Markdown

Konfiguracja bazy danych

Aby przechowywać dane użytkowników, musimy utworzyć bazę danych i podłączyć ją do naszej aplikacji.

Do tego wykorzystamy docker-compose.

Tworzymy plik docker-compose.yml w katalogu głównym projektu.

YML

Musimy pamiętać aby w pluku .env dodać połączenia z bazą danych.

Markdown

Jak wspomniałem wcześniej, użyjemy drizzle jako naszego ORM. Ponieważ korzystamy z PostgreSQL, musimy również zainstalować paczkę pg. Dodatkowo możemy zainstalować drizzle-kit, aby uzyskać wygodne GUI do zarządzania bazą danych.

typescript

Utwórzmy plik drizzle.config.ts w katalogu głównym projektu i zdefiniujmy konfigurację, ustawiając ścieżki do schema, migracji oraz dodając URL któy służy do połączenia z bazą danych.

typescript

Teraz utwórzmy katalog drizzle w folderze src, a następnie dodajmy plik schema.ts, aby zdefiniować strukturę danych w bazie.

typescript

Musimy także utworzyć skrypt migracji.

typescript

Teraz w końcu możemy zasmakować magii NestJS.

Aby korzystać z połączenia z bazą danych obsługiwanego przez drizzle, musimy zdefiniować provider o nazwie drizzleProvider. Z tego, co wiem, drizzle nie oferuje żadnej integracji z NestJS, która zawierałaby taki provider, więc musimy stworzyć własny.

typescript

Custom providers w NestJS działają jak argumenty przekazywane do konstruktora. W czasie działania aplikacji NestJS odczytuje token i wstrzykuje provider do każdej klasy, w której token DrizzleProvider został jawnie dodany do dekoratora @Inject().

Rola funkcji useFactory polega na dynamicznym tworzeniu wartości mając dostęp do wstrzykiwanych zależności – w naszym przypadku ConfigService

Musimy przekazać kilka właściwości do obiektu przechowywanego w tablicy providers:

  • provide Ciąg znaków pełniący rolę tokenu używanego przez system DI.
  • inject Tablica zależności (klas) wymaganych w providerze. W naszym przypadku jest to ConfigService, który zapewnia dostęp do zmiennych środowiskowych.
  • useFactory Funkcja generująca wartość do wstrzyknięcia do innych klas, mająca dostęp do wstrzykniętych zależności(poprzez tablicę inject). Tutaj pobieramy adres URL bazy danych za pomocą ConfigService, używamy go do nawiązania połączenia i zwracamy wrapper drizzle do interakcji z bazą danych.

Ostatnim krokiem konfiguracji Drizzle jest utworzenie DrizzleModule.

typescript

Gdy provider jest już skonfigurowany, musimy zarejestrować go w app.module.ts i udostępnić globalnie w naszej aplikacji.

typescript

Google auth strategy

Ponieważ nasze uwierzytelnienie opiera się na usłudze Google, musimy utworzyć dla niej strategię Google w Passport.

Na początek zainstalujmy wymagane zależności, takie jak Passport, strategię google z Passport oraz utilities dla NestJS.

terminal

Nasza strategia Google to po prostu wstrzykiwalne rozszerzenie klasy PassportStrategy przygotowane dla NestJS.

typescript

Prawie wszystko jest obsługiwane za nas out od the box. Musimy jedynie przekazać konfigurację z danymi uwierzytelniającymi Google i zdefiniować metodę validate, która w tym przypadku wyciąga i formatuje dane użytkownika Google. Choć dodanie walidacji (np. za pomocą biblioteki takiej jak Zod) może być dobrym pomysłem, pominiemy tą kwestę, aby zachować prostotę. Korzystamy także z wstrzykiwania zależności, aby pobrać dane uwierzytelniające z ConfigService.

Aby użyć naszej strategii w kontrolerze, musimy zdefiniować rozszerzenie klasy, które pozwoli nam to zrobić, a jest nim AuthGuard.

typescript

Ta klasa działa jak spust oraz zamyka całą logikę uwierzytelniania. Nie musimy implementować żadnych niestandardowych rozwiązań, ponieważ mamy gotową klasę eksportowaną z paczki @nestjs/passport.

Po przygotowaniu klas strategii i guard'a, musimy dodać je do pliku auth.module.ts w tablicy providers, aby były dostępne do wstrzykiwania zależności w ramach modułu autoryzacji.

typescript

Moduł User'a

Aby przeprowadzić uwierzytelnianie, musimy zaimplementować logikę przechowywania i zarządzania użytkownikami. Stwórzmy więc klasę o nazwie UserRepository w pliku user.repository.ts.

typescript

W tej klasie mamy kilka metod zarządzających encją użytkownika. Ciekawa część dotyczy definicji konstruktora, gdzie przekazujemy token DrizzleProvider do dekoratora @Inject(), aby powiedzieć NestJS, aby wstrzyknął wartość do klasy. Wspominałem o tym wcześniej w sekcji konfiguracji bazy danych. Dzięki tej logice, nasz parametr db będzie przechowywał połączenie z bazą danych. Możliwe, że zauważyłeś, że nasz niestandardowy provider drizzle nie jest klasą. Gdyby nią był, nie musielibyśmy używać dekoratora @Inject(), ponieważ system DI NestJS wiedziałby, którą klasę wstrzyknąć, patrząc tylko na typ przypisany do parametru konstruktora.

Dodajmy naszą klasę repozytorium do tablicy exports w pliku module. Dodatkowo, musimy upewnić się, że nasz niestandardowy provider jest poprawnie zaimportowany.

typescript

JWT Strategy

Zanim stworzymy usługę uwierzytelniania i dodamy kontrolery, dodajmy dodatkową strategię Passport, która będzie zarządzać sesją zalogowanego użytkownika.

Zainstalujmy wymagane zależności:

terminal

Dodaj sekret wymagany do tworzenia tokenu dostępu:

Markdown

Następnie zdefiniujmy strategię JWT.

typescript

Będziemy przechowywać token w cookie, więc w strategii musimy określić, w jaki sposób będziemy go wyciągać, używając ConfigService wstrzykniętego do strategii. Token będzie automatycznie przekształcany. W metodzie validate możemy wywołać jedną z metod UserRepository, aby przeprowadzić prostą walidację, sprawdzając, czy użytkownik przypisany do tokena istnieje w bazie danych.

Aby użyć naszej strategii, musimy zdefiniować auth guard, stosując ten sam wzorzec, co w przypadku uwierzytelniania Google.

typescript

Uaktualnijmy auth.module.ts

typescript

Wraz z klasą związaną z JWT musimy dodać nasz UserModule do tablicy imports, aby móc używać UserRepository w auth module.

Logika uwierzytelnienia(AuthService)

Warstwa service zwykle zawiera logikę biznesową, więc powinniśmy umieścić tam całą logikę związaną z rejestracją użytkownika, logowaniem, wylogowywaniem, sprawdzaniem użytkownika i tworzeniem tokenów.

typescript

AuthService wykorzystuje trzy wstrzykiwane klasy. Wzorce oferowane przez NestJS umożliwiają komponowanie kodu niczym klocki Lego.

Tworzenie API(Controllers)

Teraz nadszedł czas, aby uczynić naszą aplikację funkcjonalną. Stworzymy dwa kontrolery:

  • AutController - do uwierzytelnienia
  • UserController - do zarządzania użytkownikami

Zacznijmy od AuthController.

typescript

Aby zdefiniować kontroler, używamy dekoratora @Controller(), przekazując stringa, który reprezentuje nazwę ścieżki. Wstrzykujemy AuthService do kontrolera, ponieważ potrzebujemy go do obsługi logiki uwierzytelnienia. Następnie używamy dekoratora @Get('google'), aby zdefiniować punkt wejścia dla logowania. Poniżej dodajemy kolejny dekorator: @UseGuards(GoogleGuard), aby zastosować wcześniej przygotowanego guarda. Informuje on NestJS o potrzebie uruchomienia logiki uwierzytelnienia. Następnie użytkownik jest przekierowywany na ekran logowania Google. Po pomyślnym zalogowaniu użytkownik jest przekierowywany do google/callback, gdzie guard obsługuje walidację i komunikację z Google IDP w celu pobrania informacji o profilu użytkownika. Po tym wszystkim możemy wywołać metodę signIn, aby wygenerować access token, a następnie przekierować użytkownika do chronionej ścieżki z tokenem w cookies.

Gdy nasz AuthController jest już skończony, musimy go zarejestrować i wyeksportować z modułu. Zaktualizujmy więc plik auth.module.ts. Warto także wyeksportować JwtAuthGuard, ponieważ będzie on wymagany w module użytkownika do zabezpieczenia endpointów.

typescript

Podążając ścieżką użytkownika, tworzymy UserController, aby pobierać i zwracać użytkowników z chronionych endpointów.

typescript

Podobnie jak w AuthController, tutaj również użyjemy guarda, tym razem nie do uwierzytelniania użytkownika, lecz do ochrony endpointów. Jeśli token jest nieprawidłowy, żadna z funkcji w kontrolerze nie zostanie uruchomiona, a zamiast tego zostanie wyrzucony błąd: UnauthorizedException.

Ostatni krok

Gdy pokryjemy wszystkie przypadki i stworzymy wszystkie elementy składowe naszej aplikacji, musimy dodać AuthModule i UserModule do app.module.ts.

typescript

Wnioski

Na pierwszy rzut oka Nest.js może wydawać się nieco skomplikowany i trudny do zrozumienia, ale gdy zagłębimy się w jego podstawowe koncepcje, zobaczymy, jak łatwo można za jego pomocą budować aplikacje. W przypadkach, gdy potrzebujemy szybko uruchomić mały serwer, nie jest to najlepszy wybór, ale dla dużych projektów wymagających dobrej skalowalności ten framework jest doskonałym rozwiązaniem. Programiści zaznajomieni z Nest.js mogą szybko stać się produktywni po dołączeniu do projektu rozwijanego w tym frameworku. Nest.js zachęca do pisania dobrze ustrukturyzowanego kodu. Bez wątpienia jego popularność będzie nadal rosła, czyniąc go rozsądnym wyborem do tworzenia skalowalnych aplikacji backendowych w Node.js.

Dzięki za przeczytanie, zpraszam do śledzenia kolejnych artykułów! 👋

PS: Kod możesz znaleść tutaj na GitHub`ie

typescript and javascript logogreg@aboutjs.dev

©Grzegorz Dubiel | 2026