StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

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

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

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

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

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

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • Audyt SEO i Performance
  • Testy automatyczne i QA
  • Konsultacje Produktowe
  • Automatyzacja Procesów AI
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
Next.jsBackendArchitektura

Wzorzec Repository + Service Layer w Next.js full-stack — backend, który nie boli

Backend w Next.js zamienia się w spaghetti szybciej niż się wydaje. Repository + Service Layer ogranicza chaos, jeśli dobrze ustawisz transakcje, walidację i granice warstw.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
10 kwietnia 2026 10:50
Czytanie
5 min czytania
Aktualizacja
11 czerwca 2026 13:00

Problem: logika biznesowa w Server Actions

Budujesz full-stack w Next.js i logika ląduje tam, gdzie najwygodniej — prosto w Server Actions i Route Handlers. Na początku działa bez zarzutu. Po kilku miesiącach jedna funkcja miesza walidację, dostęp do bazy, reguły biznesowe, transakcje i rewalidację cache, a Ty boisz się ją tknąć. To jest moment, w którym warstwy przestają być akademicką teorią.

Artykuł w skrócie

  • Trzy warstwy o jasnych rolach — repository (dostęp do danych), service (reguły biznesowe), Server Actions i Route Handlers jako cienkie adaptery wejścia. Każda wie o sobie jak najmniej.
  • Repository skupia zapytania w jednym miejscu — zmiana schematu czy wymiana ORM (Prisma → Drizzle) to edycja implementacji repository, a nie przepisywanie całej aplikacji.
  • Service nie wie o HTTP, ale zna przypadek użycia — zawiera reguły biznesowe, orkiestrację i decyzję o transakcji; dzięki temu testujesz logikę bez bazy, podstawiając fałszywe repository.
  • Transakcja obejmuje cały przypadek użycia — utworzenie zamówienia i aktualizacja stanu magazynowego muszą zakończyć się razem albo razem się wycofać.
  • Ten sam serwis obsługuje wiele wejść — Server Action, Route Handler i zadanie cykliczne wywołują tę samą metodę serwisu, więc logika biznesowa istnieje raz, bez duplikacji.
  • Czyste funkcje dla logiki obliczeniowej — np. kalkulacja ceny jako osobna, pozbawiona efektów ubocznych funkcja, którą trywialnie pokrywasz testami.
  • To nie przerost, ale nie zawsze — dla CRUD z dwiema encjami to za dużo; sięgaj po wzorzec, gdy pojawiają się reguły biznesowe albo ta sama logika potrzebna w wielu miejscach.
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 } },
  })
 
  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 — abstrakcja dostępu do danych

Repository to warstwa (obiekt lub zestaw funkcji), która enkapsuluje cały dostęp do bazy danych. Reszta aplikacji nie wykonuje zapytań bezpośrednio — woła metody repository jak findById czy create. Dzięki temu zapytania ORM są w jednym miejscu i można podmienić bazę albo ORM bez ruszania logiki biznesowej. to obiekt (lub zestaw funkcji), który enkapsuluje dostęp do bazy danych. Zapytania SQL/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[]> {
      return client.product.findMany({
        where: {
          category: filters?.category,
          price: {
            gte: filters?.minPrice,
            lte: filters?.maxPrice,
          },
        },
        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 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 to zmiana implementacji repository, nie całej aplikacji
  • 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.

Unit of Work — 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 — logika biznesowa

Service Layer (warstwa serwisowa) to miejsce na reguły biznesowe i orkiestrację operacji — co i w jakiej kolejności ma się wydarzyć. Korzysta z repository do dostępu do danych, ale celowo nie wie nic o bazie, ORM ani HTTP, dzięki czemu jest czysta i testowalna w izolacji. zawiera reguły biznesowe, walidację i orkiestrację operacji. Serwis korzysta z repository do dostępu do danych, ale nie wie nic o bazie danych, ORM ani HTTP.

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

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.

Wspólna walidacja wejścia

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 — 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 — ten sam serwis, inne wejście

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 })
}

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

Testowanie

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

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ć tego wzorca

Nie każdy projekt potrzebuje pełnego zestawu warstw. Jeśli masz małą stronę z panelem administracyjnym, trzy tabele i proste operacje CRUD, 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”.

Werdykt Labu

Repository i Service Layer to nie przerost formy, jeśli stosujesz je w odpowiednim momencie. To inwestycja w utrzymywalność, która separuje trzy rzeczy zbyt często wymieszane w jednej funkcji: dostęp do danych, reguły biznesowe i interfejs wejścia. Repository skupia zapytania, service trzyma logikę i transakcje, a Server Actions oraz Route Handlers stają się cienkimi adapterami. Efekt jest namacalny — logikę testujesz bez bazy, wymiana ORM nie dotyka reguł biznesowych, a ten sam serwis obsługuje Server Action, Route Handler i zadanie cykliczne bez duplikacji.

Klucz to wyczucie skali. Dla CRUD z dwiema encjami ten wzorzec jest przerostem; sięgaj po niego, gdy pojawia się prawdziwa logika biznesowa — ceny, stany, reguły — albo gdy ta sama operacja potrzebna jest w dwóch miejscach. Nie musisz wprowadzać go w całej aplikacji naraz: zacznij od domeny, w której Server Actions zaczynają puchnąć i mieszać odpowiedzialności, a resztę refaktoryzuj, gdy zaboli. Tak zastosowany, wzorzec naprawdę sprawia, że backend przestaje boleć.

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
Next.js
  • Problem: logika biznesowa w Server Actions2 min
  • Warstwa Repository — abstrakcja dostępu do danych1 min
  • Unit of Work — transakcja dla przypadku użycia1 min
  • Warstwa Service — logika biznesowa1 min
  • Granice odpowiedzialności1 min
  • Wspólna walidacja wejścia1 min
  • Server Actions — cienka warstwa wejścia1 min
  • Route Handlers — ten sam serwis, inne wejście1 min
  • Testowanie1 min
  • Struktura plików1 min
  • Kiedy nie używać tego wzorca1 min
  • Werdykt Labu1 min

Często zadawane pytania

Źródła i dokumentacjaZweryfikowano: 11 czerwca 2026

Wzorce dostępu do danych, Server Actions, Route Handlers, transakcje i testowanie zweryfikowano na podstawie oficjalnej dokumentacji Next.js, Prisma, Drizzle i Vitest:

Next.js: Mutating Data, Next.js: Route Handlers, Prisma: Transactions, Prisma: Best practices for the client, Drizzle ORM: dokumentacja, Vitest: Mocking.

Maciej Sala

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 rozwija własne projekty side-projecty.

Moje artykułyWięcej o mnie

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

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

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

Drizzle ORM czy Prisma w 2026? Wydajność, DX i edge — porównanie, które pomoże zdecydować, zanim napiszesz pierwszy schemat bazy danych.

Maciej Sala

Maciej Sala

Founder Strivelab

10 kwietnia 2026
Backend dla frontendowca: serwer, bazy danych i API
Backend dla frontendowca: serwer, bazy danych i API

Pierwsza część serii Backend dla frontendowca: architektura aplikacji, serwer, bazy danych, API, statusy HTTP, paginacja, idempotency, BFF i CORS.

Maciej Sala

Maciej Sala

Founder Strivelab

28 lipca 2025
GA4 Data API w Next.js – budujemy własny dashboard analityczny
GA4 Data API w Next.js – budujemy własny dashboard analityczny

GA4 Data API w Next.js bez skrótów myślowych: service account, cache, limity, bezpieczeństwo i budowa własnego dashboardu na danych z Analytics.

Maciej Sala

Maciej Sala

Founder Strivelab

31 sierpnia 2025
Poprzedni wpisNext.js 15 — co nowego i czy warto migrować z 14?Next.js 15 zmienia model cache i dodaje async params — migracja z 14 nie jest tylko aktualizacją. Co naprawdę się zmienia i czy warto?
Maciej Sala

Maciej Sala

Founder Strivelab

10 kwietnia 2026
Następny wpisDrizzle ORM vs Prisma — co wybrać w 2026 do projektu Next.js?Drizzle ORM czy Prisma w 2026? Wydajność, DX i edge — porównanie, które pomoże zdecydować, zanim napiszesz pierwszy schemat bazy danych.
Maciej Sala

Maciej Sala

Founder Strivelab

10 kwietnia 2026