Przejdź do treści

Repository i Service Layer w Next.js dla Server Actions

Jak podzielić backend w Next.js na Repository, Service Layer i Unit of Work? Praktyczny wzorzec dla Server Actions, Route Handlers, transakcji i testów.

Maciej Sala

Founder StriveLab

7 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 26 czerwca 2026)

Jak podzielić backend w Next.js na warstwy?

Kiedy Repository i Service Layer mają sens w Next.js?

Najpierw rozpoznaj problem. Jeśli budujesz full-stack w Next.js i logika ląduje tam, gdzie najwygodniej, czyli prosto w Server Actions i Route Handlers, początkowo wszystko działa bez zarzutu. Po kilku miesiącach jedna funkcja miesza walidację, dostęp do bazy, reguły biznesowe, transakcje i rewalidację cache. To jest moment, w którym warstwy przestają być teorią.

Ten wzorzec ma sens szczególnie wtedy, gdy:

  • ta sama operacja jest wywoływana z formularza, Route Handlera, webhooka albo zadania cyklicznego,
  • jedna akcja musi zmienić kilka tabel atomowo,
  • logika biznesowa ma reguły, które nie powinny zależeć od HTTP ani Reacta,
  • chcesz testować reguły bez prawdziwej bazy danych,
  • chcesz ograniczyć wpływ przyszłej zmiany ORM.

W przykładach domeną przykładowej aplikacji jest example.com, czyli domena zarezerwowana do dokumentacji. Dzięki temu żaden adres w snippetach nie wygląda jak realna część tej strony.

Code
// ŹLE — Server Action, który robi wszystko
'use server'
 
export async function createOrder(formData: FormData) {
  const session = await auth()
  if (!session) throw new Error('Unauthorized')
 
  const productId = formData.get('productId') as string
  const quantity = Number(formData.get('quantity'))
 
  // Walidacja
  if (!productId || quantity < 1) throw new Error('Invalid data')
 
  // Logika biznesowa + dostęp do bazy — wymieszane
  const product = await prisma.product.findUnique({ where: { id: productId } })
  if (!product) throw new Error('Product not found')
  if (product.stock < quantity) throw new Error('Not enough stock')
 
  const total = product.price * quantity
  const discount = total > 500 ? 0.1 : 0
  const finalPrice = total * (1 - discount)
 
  const order = await prisma.order.create({
    data: {
      userId: session.user.id,
      productId,
      quantity,
      total: finalPrice,
      status: 'PENDING',
    },
  })
 
  await prisma.product.update({
    where: { id: productId },
    data: { stock: { decrement: quantity } },
  })
 
  // Odświeża widoki w przykładowej aplikacji example.com.
  revalidatePath('/orders')
  return { success: true, orderId: order.id }
}

Problemy: nie da się tego testować bez bazy danych, logika cenowa jest ukryta w Server Action, zmiana ORM wymaga przepisania całej funkcji, duplikacja gdy potrzebujesz tego samego w Route Handlerze.

Warstwa Repository jako abstrakcja dostępu do danych

to obiekt (lub zestaw funkcji), który enkapsuluje dostęp do bazy danych. Zapytania /ORM są w jednym miejscu — reszta aplikacji operuje na czystych interfejsach TypeScript.

Code
// repositories/create-repositories.ts
import type { Prisma, PrismaClient } from '@prisma/client'
import { db } from '@/lib/db'
import type { CreateOrderData, Order, Product } from '@/types'
 
type DbClient = PrismaClient | Prisma.TransactionClient
 
export function createRepositories(client: DbClient = db) {
  return {
    product: createProductRepository(client),
    order: createOrderRepository(client),
  }
}
 
export type Repositories = ReturnType<typeof createRepositories>
 
function createProductRepository(client: DbClient) {
  return {
    async findById(id: string): Promise<Product | null> {
      return client.product.findUnique({ where: { id } })
    },
 
    async findMany(filters?: {
      category?: string
      minPrice?: number
      maxPrice?: number
    }): Promise<Product[]> {
      const where: Prisma.ProductWhereInput = {}
 
      if (filters?.category) {
        where.category = filters.category
      }
 
      if (filters?.minPrice !== undefined || filters?.maxPrice !== undefined) {
        where.price = {
          ...(filters.minPrice !== undefined ? { gte: filters.minPrice } : {}),
          ...(filters.maxPrice !== undefined ? { lte: filters.maxPrice } : {}),
        }
      }
 
      return client.product.findMany({
        where,
        orderBy: { createdAt: 'desc' },
      })
    },
 
    async decrementStockIfAvailable(id: string, quantity: number) {
      return client.product.updateMany({
        where: {
          id,
          stock: { gte: quantity },
        },
        data: {
          stock: { decrement: quantity },
        },
      })
    },
 
    async incrementStock(id: string, quantity: number): Promise<Product> {
      return client.product.update({
        where: { id },
        data: { stock: { increment: quantity } },
      })
    },
  }
}
 
function createOrderRepository(client: DbClient) {
  return {
    async findById(id: string): Promise<Order | null> {
      return client.order.findUnique({
        where: { id },
        include: { product: true, user: true },
      })
    },
 
    async findByUserId(userId: string): Promise<Order[]> {
      return client.order.findMany({
        where: { userId },
        include: { product: true },
        orderBy: { createdAt: 'desc' },
      })
    },
 
    async create(data: CreateOrderData): Promise<Order> {
      return client.order.create({ data })
    },
 
    async updateStatus(id: string, status: string): Promise<Order> {
      return client.order.update({
        where: { id },
        data: { status },
      })
    },
  }
}

Korzyści wzorca Repository

  • Jedno miejsce na zapytania — zmiana schematu bazy wymaga edycji w jednym pliku
  • Testowalność — mockujesz repository, nie całą bazę danych
  • Wymienialność — przejście z Prisma na Drizzle dotyka głównie implementacji repository, jeśli reszta aplikacji korzysta z neutralnych typów i metod
  • CzytelnośćproductRepository.findById(id) jest jaśniejsze niż surowe zapytanie Prisma

Zwróć uwagę na parametr client. Repository może działać na globalnym db, ale może też dostać klienta transakcyjnego tx. To detal, który decyduje, czy wzorzec nadaje się do realnego backendu, czy tylko ładnie wygląda na diagramie.

W przykładzie filtry mają wartości opcjonalne, dlatego where powstaje warunkowo. Nie opieraj ważnej logiki filtrowania na przypadkowym przekazywaniu undefined: w zależności od konfiguracji Prisma może pominąć takie pole, a przy strictUndefinedChecks wymagać jawnego Prisma.skip.

Unit of Work i transakcja dla przypadku użycia

Jeśli operacja biznesowa dotyka kilku tabel, transakcja powinna obejmować cały przypadek użycia. Nie chcesz sytuacji, w której zamówienie powstało, ale stan magazynowy nie został zmniejszony.

Code
// lib/unit-of-work.ts
import { db } from '@/lib/db'
import {
  createRepositories,
  type Repositories,
} from '@/repositories/create-repositories'
 
export interface UnitOfWork {
  transaction<T>(
    callback: (repositories: Repositories) => Promise<T>,
  ): Promise<T>
}
 
export const unitOfWork: UnitOfWork = {
  async transaction(callback) {
    return db.$transaction(async (tx) => {
      const repositories = createRepositories(tx)
      return callback(repositories)
    })
  },
}

To jest prosty wariant wzorca Unit of Work: service nie musi znać szczegółów Prisma, ale może powiedzieć „ta operacja ma być atomowa”.

Warstwa Service i logika biznesowa

zawiera reguły biznesowe, walidację biznesową i orkiestrację operacji. Serwis korzysta z repository do dostępu do danych, ale nie wie nic o bazie danych, ORM ani HTTP. Walidację kształtu danych wejściowych zostaw adapterom, czyli Server Action, Route Handlerowi, webhookowi albo zadaniu cyklicznemu.

Code
// services/order-service.ts
import { unitOfWork, type UnitOfWork } from '@/lib/unit-of-work'
import type { Order } from '@/types'
 
interface CreateOrderInput {
  userId: string
  productId: string
  quantity: number
}
 
interface CreateOrderResult {
  success: boolean
  order?: Order
  error?: string
}
 
interface OrderServiceDeps {
  unitOfWork: UnitOfWork
}
 
export function createOrderService({ unitOfWork }: OrderServiceDeps) {
  return {
    async createOrder(input: CreateOrderInput): Promise<CreateOrderResult> {
      // 1. Walidacja biznesowa
      if (input.quantity < 1 || input.quantity > 100) {
        return { success: false, error: 'Ilość musi być między 1 a 100' }
      }
 
      return unitOfWork.transaction(async (repositories) => {
        // 2. Sprawdzenie dostępności produktu
        const product = await repositories.product.findById(input.productId)
        if (!product) {
          return { success: false, error: 'Produkt nie istnieje' }
        }
 
        if (product.stock < input.quantity) {
          return { success: false, error: `Dostępne sztuki: ${product.stock}` }
        }
 
        // 3. Logika cenowa
        const pricing = calculatePricing(product.price, input.quantity)
 
        // 4. Atomowe zmniejszenie stanu magazynowego
        const stockUpdate =
          await repositories.product.decrementStockIfAvailable(
            input.productId,
            input.quantity,
          )
 
        if (stockUpdate.count === 0) {
          return { success: false, error: 'Produkt właśnie się wyprzedał' }
        }
 
        // 5. Utworzenie zamówienia w tej samej transakcji
        const order = await repositories.order.create({
          userId: input.userId,
          productId: input.productId,
          quantity: input.quantity,
          total: pricing.finalPrice,
          status: 'PENDING',
        })
 
        return { success: true, order }
      })
    },
 
    async cancelOrder(
      orderId: string,
      userId: string,
    ): Promise<CreateOrderResult> {
      return unitOfWork.transaction(async (repositories) => {
        const order = await repositories.order.findById(orderId)
 
        if (!order) {
          return { success: false, error: 'Zamówienie nie istnieje' }
        }
 
        if (order.userId !== userId) {
          return { success: false, error: 'Brak uprawnień' }
        }
 
        if (order.status !== 'PENDING') {
          return { success: false, error: 'Zamówienie nie może być anulowane' }
        }
 
        await repositories.product.incrementStock(
          order.productId,
          order.quantity,
        )
        const updated = await repositories.order.updateStatus(
          orderId,
          'CANCELLED',
        )
 
        return { success: true, order: updated }
      })
    },
  }
}
 
export const orderService = createOrderService({ unitOfWork })
 
// Czysta funkcja — łatwa do testowania
function calculatePricing(unitPrice: number, quantity: number) {
  const subtotal = unitPrice * quantity
  const discountRate = subtotal > 500 ? 0.1 : subtotal > 200 ? 0.05 : 0
  const discount = subtotal * discountRate
  const finalPrice = subtotal - discount
 
  return { subtotal, discountRate, discount, finalPrice }
}

Ten serwis nadal jest prosty, ale ma trzy ważne cechy: operacja zamówienia jest atomowa, zapobieganie oversellingowi jest w zapytaniu aktualizującym stock, a zależności można podmienić w teście bez mockowania importów modułów.

Granice odpowiedzialności między Repository i Service Layer

Ten podział działa tylko wtedy, gdy każda warstwa robi swoją część pracy:

WarstwaOdpowiedzialność
Server Actionformularz, sesja użytkownika, walidacja inputu, rewalidacja cache
Route HandlerHTTP, statusy odpowiedzi, walidacja JSON, mapowanie błędów na response
Servicereguły biznesowe, orkiestracja, decyzja o transakcji
Repositoryzapytania do bazy i szczegóły ORM
Czyste funkcje domenoweobliczenia bez efektów ubocznych, np. cena, rabat, limity
revalidatePath / cachena zewnątrz service, bo to szczegół interfejsu Next.js

Jeśli service zaczyna importować revalidatePath, Request, Response albo cookies, granica pęka. Jeśli repository zaczyna decydować, czy użytkownik może anulować zamówienie, granica też pęka.

Najczęstsze błędy w Repository i Service Layer

Największe problemy nie wynikają z samego wzorca, tylko z pomieszania granic:

  • Transakcja zamknięta w pojedynczym repository — wtedy nie obejmuje całego przypadku użycia. Jeśli zamówienie i aktualizacja stanu magazynowego mają być atomowe, transakcję otwiera service przez Unit of Work.
  • Service importuje API Next.jsrevalidatePath, cookies, headers, Request i Response należą do adapterów wejścia, nie do logiki biznesowej.
  • Repository zwraca przypadkowe typy ORM — to szybkie na starcie, ale ogranicza wymienialność. Jeśli niezależność od ORM jest ważna, zwracaj typy domenowe.
  • Walidacja jest zduplikowana w kilku wejściach — Server Action i Route Handler mogą mieć różny protokół, ale powinny kończyć z tym samym sprawdzonym DTO.
  • Warstwy powstają za wcześnie — jeśli masz jeden formularz i jedno zapytanie, prosta funkcja będzie lepsza niż katalogi tworzone na zapas.

Wspólna walidacja wejścia w backendzie Next.js

Server Action i Route Handler mogą mieć różne protokoły wejścia, ale powinny kończyć z tym samym bezpiecznym typem domenowym. Najprościej wynieść schemat Zod do wspólnego pliku:

Code
// schemas/order-schema.ts
import { z } from 'zod'
 
export const createOrderSchema = z.object({
  productId: z.string().uuid(),
  quantity: z.coerce.number().int().min(1).max(100),
})
 
export type CreateOrderDto = z.infer<typeof createOrderSchema>

Server Actions jako cienka warstwa wejścia

Po separacji Server Actions stają się cienką warstwą wejścia dla formularzy i mutacji Reacta: autoryzacja, walidacja inputu, wywołanie serwisu, rewalidacja cache.

Code
// actions/order-actions.ts
'use server'
 
import { auth } from '@/lib/auth'
import { revalidatePath } from 'next/cache'
import { orderService } from '@/services/order-service'
import { createOrderSchema } from '@/schemas/order-schema'
 
export async function createOrderAction(formData: FormData) {
  // 1. Autoryzacja
  const session = await auth()
  if (!session) return { error: 'Musisz być zalogowany' }
 
  // 2. Walidacja inputu
  const parsed = createOrderSchema.safeParse({
    productId: formData.get('productId'),
    quantity: formData.get('quantity'),
  })
 
  if (!parsed.success) {
    return { error: 'Nieprawidłowe dane', details: parsed.error.flatten() }
  }
 
  // 3. Delegacja do serwisu
  const result = await orderService.createOrder({
    userId: session.user.id,
    ...parsed.data,
  })
 
  // 4. Rewalidacja cache
  if (result.success) {
    revalidatePath('/orders')
    revalidatePath('/products')
  }
 
  return result
}
 
export async function cancelOrderAction(orderId: string) {
  const session = await auth()
  if (!session) return { error: 'Musisz być zalogowany' }
 
  const result = await orderService.cancelOrder(orderId, session.user.id)
 
  if (result.success) {
    revalidatePath('/orders')
  }
 
  return result
}

Route Handlers i ten sam serwis z innym wejściem

Code
// app/api/orders/route.ts
import { auth } from '@/lib/auth'
import { orderService } from '@/services/order-service'
import { createOrderSchema } from '@/schemas/order-schema'
 
export async function POST(request: Request) {
  const session = await auth()
  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }
 
  const body = await request.json()
  const parsed = createOrderSchema.safeParse(body)
 
  if (!parsed.success) {
    return Response.json(
      { error: 'Invalid payload', details: parsed.error.flatten() },
      { status: 422 },
    )
  }
 
  const result = await orderService.createOrder({
    userId: session.user.id,
    ...parsed.data,
  })
 
  if (!result.success) {
    return Response.json({ error: result.error }, { status: 400 })
  }
 
  return Response.json(result.order, { status: 201 })
}

Publiczne wywołanie tego endpointu w dokumentacji pokazuj na domenie zarezerwowanej do przykładów:

Code
curl -X POST https://example.com/api/orders \
  -H 'Content-Type: application/json' \
  -d '{"productId":"2f4f2b1e-9f4a-4a8a-b5f5-8df9e6c3a901","quantity":2}'

Server Action i Route Handler używają tego samego serwisu — logika biznesowa istnieje w jednym miejscu.

Testowanie Repository i Service Layer

Największa korzyść tej architektury: testowanie bez bazy danych.

Code
// __tests__/services/order-service.test.ts
import { createOrderService } from '@/services/order-service'
import { vi, describe, it, expect, beforeEach } from 'vitest'
 
describe('orderService.createOrder', () => {
  const productRepository = {
    findById: vi.fn(),
    decrementStockIfAvailable: vi.fn(),
    incrementStock: vi.fn(),
  }
 
  const orderRepository = {
    findById: vi.fn(),
    create: vi.fn(),
    updateStatus: vi.fn(),
  }
 
  const unitOfWork = {
    transaction: vi.fn(async (callback) =>
      callback({
        product: productRepository,
        order: orderRepository,
      }),
    ),
  }
 
  const orderService = createOrderService({ unitOfWork })
 
  beforeEach(() => {
    vi.clearAllMocks()
  })
 
  it('should create order when product is available', async () => {
    productRepository.findById.mockResolvedValue({
      id: '1',
      name: 'Test',
      price: 100,
      stock: 10,
      category: 'test',
    })
 
    productRepository.decrementStockIfAvailable.mockResolvedValue({ count: 1 })
 
    orderRepository.create.mockResolvedValue({
      id: 'order-1',
      userId: 'user-1',
      productId: '1',
      quantity: 2,
      total: 200,
      status: 'PENDING',
    })
 
    const result = await orderService.createOrder({
      userId: 'user-1',
      productId: '1',
      quantity: 2,
    })
 
    expect(result.success).toBe(true)
    expect(result.order?.total).toBe(200)
    expect(productRepository.decrementStockIfAvailable).toHaveBeenCalledWith(
      '1',
      2,
    )
    expect(unitOfWork.transaction).toHaveBeenCalledTimes(1)
  })
 
  it('should fail when product out of stock', async () => {
    productRepository.findById.mockResolvedValue({
      id: '1',
      name: 'Test',
      price: 100,
      stock: 1,
      category: 'test',
    })
 
    const result = await orderService.createOrder({
      userId: 'user-1',
      productId: '1',
      quantity: 5,
    })
 
    expect(result.success).toBe(false)
    expect(result.error).toContain('Dostępne sztuki')
    expect(orderRepository.create).not.toHaveBeenCalled()
  })
})

Struktura plików dla backendu Next.js

Code
src/
├── repositories/        ← Dostęp do danych (ORM)
│   └── create-repositories.ts
├── services/            ← Logika biznesowa
│   ├── order-service.ts
│   ├── pricing-service.ts
│   └── notification-service.ts
├── schemas/             ← Wspólna walidacja wejścia
│   └── order-schema.ts
├── actions/             ← Server Actions (cienkie adaptery)
│   ├── order-actions.ts
│   └── product-actions.ts
├── lib/
│   ├── db.ts
│   └── unit-of-work.ts  ← Transakcje dla przypadków użycia
├── app/
│   ├── api/             ← Route Handlers (cienkie adaptery)
│   └── ...
└── types/               ← Interfejsy i typy
    └── index.ts

Kiedy nie używać Repository i Service Layer?

Nie każdy projekt potrzebuje pełnego zestawu warstw. Jeśli masz małą stronę z panelem administracyjnym, trzy tabele i proste operacje , zacznij od prostszej struktury:

  • schema Zod blisko Server Action,
  • jedno zapytanie Prisma/Drizzle w funkcji,
  • brak osobnego service, dopóki nie ma reguł biznesowych,
  • refaktor dopiero wtedy, gdy ta sama logika pojawia się w drugim miejscu.

Wzorzec Repository + Service Layer ma sens, gdy pojawiają się transakcje, kilka wejść do tej samej operacji, integracje zewnętrzne, reguły stanów albo realne testy jednostkowe logiki domenowej. Najpierw funkcja, potem warstwa — to często zdrowsza ścieżka niż budowanie katalogów „na zapas”.

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
Next.js

Często zadawane pytania

Czy to nie za dużo abstrakcji dla małego projektu?

Dla prostego CRUD z dwiema czy trzema encjami — tak, to zwykle przerost formy nad treścią i lepiej zostać przy bezpośrednich zapytaniach w Server Action. Wzorzec zaczyna się opłacać, gdy pojawia się realna logika biznesowa: kalkulacja cen, stany magazynowe, powiadomienia, reguły przejść statusów albo ta sama operacja wywoływana z formularza, API i joba. Wtedy warto wprowadzić separację wcześnie, zanim logika rozproszy się po wielu funkcjach i stanie się trudna do zmiany.

Kiedy wprowadzić warstwy repository i service?

Dobrym sygnałem jest moment, w którym Server Action albo Route Handler przekracza mniej więcej 30 linii i miesza w sobie walidację, dostęp do bazy i reguły biznesowe — albo gdy zauważasz, że ta sama logika jest potrzebna w dwóch miejscach. To wtedy duplikacja i splątanie zaczynają realnie spowalniać pracę, a separacja warstw się zwraca. Nie musisz refaktoryzować całej aplikacji naraz; zacznij od domeny, która najbardziej boli.

Czy repository powinno zwracać typy z Prisma?

Docelowo nie — repository powinno zwracać własne typy domenowe, niezależne od ORM, bo dopiero one tworzą granicę, która ogranicza wpływ ewentualnej zmiany Prismy na Drizzle. W praktyce jednak na początku używanie typów generowanych przez Prismę jest w pełni akceptowalne i pragmatyczne. Wprowadzasz własne typy domenowe dopiero wtedy, gdy zmiana ORM albo niezależność od niego staje się realnym wymaganiem, a nie teoretycznym.

Czym repository różni się od service?

Repository odpowiada wyłącznie za dostęp do danych — zna ORM i bazę, ale nie zna reguł biznesowych. Service zna reguły biznesowe i orkiestruje operacje, ale nie wie nic o bazie, ORM ani HTTP; korzysta z repository jak z czarnej skrzynki. Ten podział sprawia, że zapytania masz w jednym miejscu, logikę w drugim, a żadna warstwa nie musi się zmieniać, gdy zmienia się ta druga. To esencja całego wzorca.

Czy ten wzorzec działa z Drizzle, a nie tylko Prisma?

Tak, jeśli interfejs repository nie przecieka typami i idiomami konkretnego ORM. Repository jest właśnie tą warstwą, która może ukryć Prismę, Drizzle albo surowy SQL przed resztą aplikacji. Wtedy wymiana ORM sprowadza się głównie do przepisania implementacji metod repository przy zachowaniu ich sygnatur — logika biznesowa i warstwa wejścia mają dużo mniejszą powierzchnię zmian.

Gdzie trzymać transakcje?

Transakcja powinna obejmować cały przypadek użycia, a nie pojedyncze wywołanie repository. Najczęściej robi to service przez Unit of Work: otwiera transakcję, tworzy repozytoria na kliencie transakcyjnym i wykonuje wszystkie operacje atomowo. Dzięki temu utworzenie zamówienia i zmniejszenie stanu magazynowego kończą się razem albo razem się wycofują.

Czy walidacja powinna być w Server Action czy w service?

Walidacja wejścia należy do warstwy wejścia: Server Action, Route Handler, zadanie cykliczne albo webhook powinny zamienić obce dane na bezpieczny input domenowy. Reguły biznesowe, np. limity ilości, dostępność produktu, uprawnienia do anulowania zamówienia czy stany przejść, należą do service. Schemat Zod możesz współdzielić między Server Action i Route Handlerem, żeby nie rozjechały się kontrakty.

Czy Server Action jest prywatna, jeśli nie ma własnego endpointu API?

Nie. Server Action uruchamia się po stronie serwera, ale nadal trzeba traktować ją jak publiczne wejście do mutacji: walidować dane, sprawdzać sesję i egzekwować uprawnienia. To, że Next.js ukrywa szczegóły transportu, nie zwalnia z kontroli bezpieczeństwa. Właśnie dlatego Server Action powinna być cienkim adapterem, a nie miejscem, w którym mieszasz autoryzację, reguły biznesowe i zapytania do bazy.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Drizzle ORM vs Prisma — co wybrać w 2026 do projektu Next.js?

Prisma i Drizzle zdominowały świat Next.js i ogólnie Node.js, wyrastając na dwa najpotężniejsze ORM-y dostępne na rynku. Choć służą podobnemu celowi, reprezentują dwie fundamentalnie różne wizje komunikacji z bazą danych.

Maciej Sala

Maciej Sala

Founder StriveLab

Route Handlers czy Server Actions? Kiedy co wybrać

App Router daje Ci dwa sposoby na uruchomienie kodu po stronie serwera, poprzez Route Handlers i Server Actions . Może wyglądają podobnie, ponieważ oba działają na serwerze, sięgają do bazy i do zmiennych środowiskowych, ale każdy z nich rozwiązuje zupełnie inny problem. Użycie jednego tam, gdzie pasuje drugi, nie jest odpowiednim rozwiązaniem, ponieważ zły wybór odbija się potem na architekturze.

Maciej Sala

Maciej Sala

Founder StriveLab

RBAC w Next.js: Jak zabezpieczyć role i uprawnienia?

Wyobraź sobie, że dla każdego użytkownika z osobna sprawdzasz, czy wolno mu edytować artykuł. Przy dziesięciu osobach jeszcze ujdzie, przy tysiącu to droga przez mękę. RBAC odwraca pytanie: uprawnienia wiążesz z rolami admin, editor, user , role z ludźmi, a w kodzie pytasz tylko, czy rola ma uprawnienie posts:edit . Jedna reguła zamiast tysiąca wyjątków.

Maciej Sala

Maciej Sala

Founder StriveLab