Przejdź do treści

REST API WordPressa — integracja z React i Next.js

WordPress REST API z Next.js — autentykacja, custom endpointy i cache bez typowych pułapek, w które wpada każdy, kto robi to pierwszy raz.

Maciej Sala

Founder StriveLab

5 min czytaniaOpublikowano 29 stycznia 2025 (Aktualizacja 20 maja 2026)

W tym artykule pokażę jak prosto zbudować tę integrację od podstaw, a działa tu zarówno , jak i architektura . Nie wiesz jeszcze za dużo o WordPressie? Zobacz artykuł WordPressa od zera — instalacji, konfiguracji i podstaw.

Czym jest WordPress REST API?

REST API to interfejs HTTP do danych WordPressa, który zamiast renderować gotowy HTML, WordPress zwraca czysty JSON:

Code
# Pobierz posty
curl https://twojadomena.pl/wp-json/wp/v2/posts
 
# Pobierz strony
curl https://twojadomena.pl/wp-json/wp/v2/pages
 
# Pobierz media
curl https://twojadomena.pl/wp-json/wp/v2/media

W odpowiedzi dostajesz surowe dane, po które dowolny frontend w JavaScript może sięgnąć i wyrenderować je po swojemu.

Wbudowane endpointy WordPress REST API

WordPress udostępnia domyślnie:

EndpointMetodyOpis
/wp/v2/postsGET, POST, PUT, DELETEPosty
/wp/v2/pagesGET, POST, PUT, DELETEStrony
/wp/v2/mediaGET, POST, PUT, DELETEMedia
/wp/v2/categoriesGET, POST, PUT, DELETEKategorie
/wp/v2/tagsGET, POST, PUT, DELETETagi
/wp/v2/usersGET, POST, PUT, DELETEUżytkownicy
/wp/v2/commentsGET, POST, PUT, DELETEKomentarze

Przy listingach zwróć uwagę na nagłówki odpowiedzi X-WP-Total i X-WP-TotalPages, bo to z nich budujesz paginację po stronie klienta. Sam JSON zwraca tylko bieżącą stronę.

Parametry zapytań w WordPress REST API

Code
# Paginacja
/wp/v2/posts?page=2&per_page=10
 
# Filtrowanie po kategorii
/wp/v2/posts?categories=5
 
# Wyszukiwanie
/wp/v2/posts?search=javascript
 
# Sortowanie
/wp/v2/posts?orderby=date&order=desc
 
# Wybór pól (oszczędność transferu)
/wp/v2/posts?_fields=id,title,slug,excerpt
 
# Embed (dołącz powiązane dane)
/wp/v2/posts?_embed

Parametr _embed jest flagą, więc wystarczy jego obecność (?_embed), a wartość true jest ignorowana po stronie serwera, choć zadziała. W dalszych przykładach używam _embed=true dla czytelności w URLSearchParams.

Konfiguracja WordPressa pod REST API

1. Permalinki dla REST API

REST API wymaga ładnych permalinków:

Code
Ustawienia → Bezpośrednie odnośniki → Nazwa wpisu (lub dowolne inne niż "Prosty")

2. CORS w WordPress REST API

Jeśli frontend jest na innej domenie:

Code
// functions.php lub wtyczka
add_action('rest_api_init', function() {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
    add_filter('rest_pre_serve_request', function($value) {
        $origin = get_http_origin();
        $allowed_origins = [
            'http://localhost:3000',
            'https://moj-frontend.vercel.app',
        ];
 
        if (in_array($origin, $allowed_origins)) {
            header('Access-Control-Allow-Origin: ' . $origin);
            header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
            header('Access-Control-Allow-Credentials: true');
            header('Access-Control-Allow-Headers: Authorization, Content-Type');
        }
 
        return $value;
    });
});

Najważniejsze, by dopuszczać wyłącznie zaufane domeny (np. własny frontend) i nigdy nie pozwalać „każdemu” (*) na zapytania z ciasteczkami logowania. Te dwie zasady chronią przed kradzieżą sesji użytkowników.

3. Ukrycie wrażliwych danych w API

Nie zakładaj, że każde pole z odpowiedzi powinno być publiczne. Jeśli wystawiasz własne endpointy albo pola użytkowników, jawnie ograniczaj dane:

Code
add_filter('rest_prepare_user', function($response, $user, $request) {
    unset($response->data['email']);
    return $response;
}, 10, 3);

Integracja WordPress REST API z Next.js

Klient API dla WordPress REST API

Code
// lib/wordpress.ts
 
const WP_URL = process.env.WORDPRESS_URL || 'https://twojadomena.pl'
const API_URL = `${WP_URL}/wp-json/wp/v2`
 
interface WPPost {
  id: number
  slug: string
  title: { rendered: string }
  content: { rendered: string }
  excerpt: { rendered: string }
  date: string
  featured_media: number
  categories: number[]
  _embedded?: {
    'wp:featuredmedia'?: Array<{ source_url: string }>
    'wp:term'?: Array<Array<{ id: number; name: string; slug: string }>>
  }
}
 
interface FetchOptions {
  page?: number
  perPage?: number
  categories?: number[]
  search?: string
  slug?: string
}
 
export async function getPosts(options: FetchOptions = {}): Promise<WPPost[]> {
  const params = new URLSearchParams({
    _embed: 'true',
    per_page: String(options.perPage || 10),
    page: String(options.page || 1),
  })
 
  if (options.categories?.length) {
    params.set('categories', options.categories.join(','))
  }
 
  if (options.search) {
    params.set('search', options.search)
  }
 
  if (options.slug) {
    params.set('slug', options.slug)
  }
 
  const response = await fetch(`${API_URL}/posts?${params}`, {
    next: { revalidate: 60 }, // ISR: odświeżaj dane co 60 sekund
  })
 
  if (!response.ok) {
    throw new Error(`WordPress API error: ${response.status}`)
  }
 
  return response.json()
}
 
export async function getPostBySlug(slug: string): Promise<WPPost | null> {
  const posts = await getPosts({ slug })
  return posts[0] || null
}
 
export async function getCategories() {
  const response = await fetch(`${API_URL}/categories?per_page=100`, {
    next: { revalidate: 3600 },
  })
  return response.json()
}

Strona listingu wpisów z WordPressa

Code
// app/blog/page.tsx
 
import { getPosts } from '@/lib/wordpress'
import Link from 'next/link'
 
export const revalidate = 60
 
export default async function BlogPage() {
  const posts = await getPosts({ perPage: 12 })
 
  return (
    <main className="container mx-auto px-4 py-8">
      <h1 className="mb-8 text-3xl font-bold">Blog</h1>
 
      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {posts.map((post) => {
          const featuredImage =
            post._embedded?.['wp:featuredmedia']?.[0]?.source_url
 
          return (
            <article
              key={post.id}
              className="overflow-hidden rounded-lg border"
            >
              {featuredImage && (
                <img
                  src={featuredImage}
                  alt=""
                  className="h-48 w-full object-cover"
                />
              )}
              <div className="p-4">
                <h2 className="mb-2 text-xl font-semibold">
                  <Link href={'/blog/' + post.slug}>
                    <span
                      dangerouslySetInnerHTML={{ __html: post.title.rendered }}
                    />
                  </Link>
                </h2>
                <div
                  className="line-clamp-3 text-gray-600"
                  dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
                />
              </div>
            </article>
          )
        })}
      </div>
    </main>
  )
}

Strona pojedynczego posta z WordPress REST API

Code
// app/blog/[slug]/page.tsx
 
import { getPostBySlug, getPosts } from '@/lib/wordpress'
import { notFound } from 'next/navigation'
 
interface Props {
  params: Promise<{ slug: string }>
}
 
// Generowanie statycznych ścieżek
export async function generateStaticParams() {
  const posts = await getPosts({ perPage: 100 })
  return posts.map((post) => ({ slug: post.slug }))
}
 
// Metadata dla SEO
export async function generateMetadata({ params }: Props) {
  const { slug } = await params
  const post = await getPostBySlug(slug)
 
  if (!post) {
    return { title: 'Nie znaleziono' }
  }
 
  return {
    title: post.title.rendered,
    description: post.excerpt.rendered.replace(/<[^>]*>/g, '').slice(0, 160),
  }
}
 
export default async function PostPage({ params }: Props) {
  const { slug } = await params
  const post = await getPostBySlug(slug)
 
  if (!post) {
    notFound()
  }
 
  const featuredImage = post._embedded?.['wp:featuredmedia']?.[0]?.source_url
 
  return (
    <article className="container mx-auto max-w-3xl px-4 py-8">
      {featuredImage && (
        <img
          src={featuredImage}
          alt=""
          className="mb-8 h-64 w-full rounded-lg object-cover"
        />
      )}
 
      <h1
        className="mb-4 text-4xl font-bold"
        dangerouslySetInnerHTML={{ __html: post.title.rendered }}
      />
 
      <time className="mb-8 block text-gray-500">
        {new Date(post.date).toLocaleDateString('pl-PL')}
      </time>
 
      <div
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: post.content.rendered }}
      />
    </article>
  )
}

Custom endpoints w WordPress REST API

Wbudowane endpointy często nie wystarczają i wtedy warto napisać własne:

Prosty custom endpoint

Code
// functions.php lub wtyczka
 
add_action('rest_api_init', function() {
    register_rest_route('moja-api/v1', '/featured-posts', [
        'methods'  => 'GET',
        'callback' => 'get_featured_posts',
        'permission_callback' => '__return_true',
    ]);
});
 
function get_featured_posts() {
    $posts = get_posts([
        'meta_key'       => 'is_featured',
        'meta_value'     => '1',
        'posts_per_page' => 5,
    ]);
 
    return array_map(function($post) {
        return [
            'id'    => $post->ID,
            'title' => $post->post_title,
            'slug'  => $post->post_name,
            'image' => get_the_post_thumbnail_url($post->ID, 'large'),
        ];
    }, $posts);
}

Custom endpoint z parametrami

Code
register_rest_route('moja-api/v1', '/posts-by-author/(?P<author_id>\d+)', [
    'methods'  => 'GET',
    'callback' => function($request) {
        $author_id = $request['author_id'];
        $page = $request->get_param('page') ?: 1;
 
        $query = new WP_Query([
            'author'         => $author_id,
            'posts_per_page' => 10,
            'paged'          => $page,
        ]);
 
        return [
            'posts'       => array_map('format_post', $query->posts),
            'total'       => $query->found_posts,
            'total_pages' => $query->max_num_pages,
        ];
    },
    'args' => [
        'author_id' => [
            'required'          => true,
            'validate_callback' => function($param) {
                return is_numeric($param);
            },
        ],
        'page' => [
            'default'           => 1,
            'validate_callback' => function($param) {
                return is_numeric($param) && $param > 0;
            },
        ],
    ],
    'permission_callback' => '__return_true',
]);

Autentykacja w WordPress REST API

Jeśli zamierzasz dodawać, edytować lub usuwać dane (przy użyciu metod POST, PUT i DELETE), musisz się wcześniej uwierzytelnić.

Application Passwords w WordPressie

Code
Użytkownicy → Twój profil → Application Passwords → Dodaj

Użycie:

Code
const credentials = Buffer.from(
  `username:xxxx xxxx xxxx xxxx xxxx xxxx`,
).toString('base64')
 
const response = await fetch(`${API_URL}/posts`, {
  method: 'POST',
  headers: {
    Authorization: `Basic ${credentials}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: 'Nowy post',
    content: 'Treść posta...',
    status: 'publish',
  }),
})

To rozwiązanie jest sensowne głównie dla połączeń pomiędzy serwerami (np. Twój backend Next.js rozmawia z WordPressem) albo dla zaplecza redakcyjnego. Nie wysyłaj takich danych uwierzytelniających do publicznego klienta w przeglądarce.

JWT w WordPress REST API

Zainstaluj wtyczkę "JWT Authentication for WP REST API":

Code
// Pobierz token
const tokenResponse = await fetch(`${WP_URL}/wp-json/jwt-auth/v1/token`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: 'admin',
    password: 'haslo',
  }),
})
 
const { token } = await tokenResponse.json()
 
// Użyj tokena
const response = await fetch(`${API_URL}/posts`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ title: 'Test', content: '...', status: 'draft' }),
})

JWT bywa wygodne, ale to dodatkowa warstwa i dodatkowa odpowiedzialność za przechowywanie tokena, odświeżanie go i bezpieczeństwo po stronie klienta.

Dodawanie pól do odpowiedzi REST API

Custom fields z ACF lub natywne pola

Code
// Dodaj pole do odpowiedzi API
add_action('rest_api_init', function() {
    register_rest_field('post', 'reading_time', [
        'get_callback' => function($post) {
            $content = get_post_field('post_content', $post['id']);
            preg_match_all('/\p{L}+/u', wp_strip_all_tags($content), $matches);
            $word_count = count($matches[0]);
            return ceil($word_count / 200);
        },
    ]);
 
    // Jeśli używasz ACF
    register_rest_field('post', 'custom_subtitle', [
        'get_callback' => function($post) {
            return get_field('subtitle', $post['id']);
        },
    ]);
});

Teraz w odpowiedzi API:

Code
{
  "id": 123,
  "title": { "rendered": "Mój post" },
  "reading_time": 5,
  "custom_subtitle": "Podtytuł z ACF"
}

Podgląd postów WordPress w Next.js

Aby zobaczyć podgląd szkicu (draftu) przed publikacją, potrzebujesz autoryzacji:

Code
// lib/wordpress.ts
export async function getPreviewPost(id: number, token: string) {
  const response = await fetch(`${API_URL}/posts/${id}?status=draft`, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
    cache: 'no-store',
  })
 
  return response.json()
}
Code
// app/preview/[id]/page.tsx
import { getPreviewPost } from '@/lib/wordpress'
import { cookies } from 'next/headers'
 
export default async function PreviewPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const token = (await cookies()).get('wp_preview_token')?.value
 
  if (!token) {
    return <div>Brak autoryzacji</div>
  }
 
  const post = await getPreviewPost(Number(id), token)
 
  return (
    <div className="bg-yellow-100 p-4">
      <p className="font-bold">PREVIEW MODE</p>
      <article dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
    </div>
  )
}

Wydajność WordPress REST API

1. Wybieraj tylko potrzebne pola z API

Code
// Zamiast pobierać wszystko
const posts = await fetch(`${API_URL}/posts?_embed=true`)
 
// Wybierz tylko potrzebne
const posts = await fetch(`${API_URL}/posts?_fields=id,title,slug,excerpt`)

Jeśli łączysz _fields z _embed, pamiętaj, że możesz przypadkiem wyciąć sobie potrzebne _embedded albo _links.

2. Cache WordPress REST API w Next.js

Code
// ISR
const posts = await fetch(url, {
  next: { revalidate: 60 },
})
 
// Statyczne (pobierane w trakcie budowania aplikacji)
const posts = await fetch(url, {
  cache: 'force-cache',
})
 
// Bez cache
const posts = await fetch(url, {
  cache: 'no-store',
})

3. Cache po stronie WordPressa

Code
// Transient API dla kosztownych operacji
function get_featured_posts_cached() {
    $cached = get_transient('featured_posts');
 
    if ($cached !== false) {
        return $cached;
    }
 
    $posts = expensive_query_here();
    set_transient('featured_posts', $posts, HOUR_IN_SECONDS);
 
    return $posts;
}

WordPress REST API vs GraphQL przez WPGraphQL

AspektREST APIWPGraphQL
InstalacjaWbudowaneWymaga wtyczki
Elastyczność zapytańOgraniczonaPełna
Pobieranie zbędnych pólTak (overfetching)Nie
Próg wejściaNiższyWyższy
Dostępne narzędziaStandardowe (fetch)Apollo, urql
CacheStandardowy HTTPWymaga konfiguracji

Przy prostych projektach REST API w zupełności wystarczy, a po WPGraphQL warto sięgnąć dopiero, gdy zapytania robią się złożone.

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

Często zadawane pytania

Jak włączyć WordPress REST API i sprawdzić, czy działa?

REST API jest domyślnie włączone od WordPressa 4.7, a warunkiem by działało są włączone ładne permalinki (Ustawienia → Bezpośrednie odnośniki → dowolna opcja inna niż „Prosty"). Możesz przetestować API otwierając w przeglądarce twoja-domena.pl/wp-json/wp/v2/posts. Jeśli widzisz JSON, API działa. Brak odpowiedzi lub błąd najczęściej oznacza problem z permalinkami albo blokadę przez plugin bezpieczeństwa.

Jak pobrać posty z WordPress REST API w Next.js App Router?

W Server Component użyj fetch() z opcją next: { revalidate: 60 } dla ISR (odświeżanie co 60 sekund) lub cache: 'force-cache' dla danych statycznych. Parametr _embed=true dołącza powiązane dane (featured image, kategorie) w jednym requeście. Dla stron generowanych statycznie zaimplementuj generateStaticParams, który zwróci listę slugów pobranych z API.

Jak skonfigurować CORS dla headless WordPress z frontendem na innej domenie?

Dodaj w functions.php lub wtyczce filtr rest_pre_serve_request, który ustawia nagłówki CORS tylko dla zaufanych domen z tablicy $allowed_origins. Nigdy nie pozwalaj „każdej domenie" (gwiazdka *) wysyłać zapytań z ciasteczkami logowania (Access-Control-Allow-Credentials: true). Dla środowiska lokalnego dodaj http://localhost:3000 do listy dozwolonych adresów.

Jak autentykować się do WordPress REST API przy operacjach zapisu?

Dla połączeń server-to-server (Next.js backend → WordPress) użyj Application Passwords, które tworzysz w profilu użytkownika WordPressa i przekazujesz w nagłówku Authorization: Basic base64(user:password). W praktyce używaj ich wyłącznie po HTTPS, bo Basic Auth nie szyfruje poświadczeń sam z siebie. JWT (przez wtyczkę) jest alternatywą, ale wymaga zarządzania czasem życia tokena. Nigdy nie wysyłaj danych uwierzytelniających do publicznego klienta w przeglądarce.

Jak dodać własne pola do odpowiedzi WordPress REST API?

Użyj funkcji register_rest_field() wewnątrz hooka rest_api_init. Podajesz typ posta, nazwę pola i callback get_callback, który zwraca wartość. Możesz w ten sposób dodać pola z Advanced Custom Fields (ACF), obliczone wartości (jak czas czytania) lub metadane, które normalnie nie są widoczne w API.

Kiedy wybrać REST API, a kiedy WPGraphQL?

REST API jest wbudowane, nie wymaga dodatkowych wtyczek i sprawdza się świetnie do prostych projektów pobierających posty, strony i media. WPGraphQL (wtyczka) pozwala pobierać dokładnie te dane, których potrzebujesz, bez pobierania nadmiarowych pól (tzw. overfetching), co jest ważne przy złożonych relacjach danych (np. zagnieżdżone custom post types z ACF). Jeśli zaczynasz projekt headless, zacznij od REST API, a na GraphQL przejdź, gdy poczujesz jego ograniczenia.

Jak zoptymalizować wydajność przy pobieraniu danych z WordPress REST API?

Używaj parametru _fields=id,title,slug,excerpt do pobierania tylko potrzebnych pól zamiast pełnych obiektów. Łącz _embed z _fields ostrożnie, bo możesz przypadkowo wyciąć _embedded z odpowiedzi, a po stronie WordPress cachuj kosztowne zapytania przez Transient API (set_transient()). Po stronie Next.js dobieraj opcję revalidate do tempa zmian treści, ustawiając dłuższy czas dla rzadko zmienianych danych (3600 s dla kategorii) i krótszy dla postów.

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
REST API — zasady projektowania i dobre praktyki

REST Representational State Transfer to styl architektoniczny, który mocno wpłynął na web, a w praktyce większość API, z którymi pracujesz jako frontend deweloper, to raczej HTTP API inspirowane REST niż "czysty REST" z podręcznika.

Maciej Sala

Maciej Sala

Founder StriveLab

Headless WordPress z Next.js — kiedy ma sens, a kiedy nie

Headless WordPress brzmi jak przepis na nowoczesną architekturę: WordPress do zarządzania treścią, Next.js do frontendu, lepsza wydajność i więcej kontroli. Problem w tym, że ten układ potrafi być świetnym wyborem albo kosztownym przerostem formy, zależnie od rodzaju projektu.

Maciej Sala

Maciej Sala

Founder StriveLab

Next.js czy WordPress w 2026 roku? Co wybrać

Z jednej strony mamy WordPressa — weterana, który wciąż napędza ponad 40% globalnego internetu. Z drugiej Next.js, flagowy framework wśród nowoczesnych technologii opartych na Reakcie. Cel obu narzędzi jest ten sam, by wypuścić stronę w świat. Różnią się jednak diametralnie drogą do tego celu, a wybór między nimi to kwestia tego, co konkretny projekt ma osiągnąć i kto będzie go utrzymywał.

Maciej Sala

Maciej Sala

Founder StriveLab