WordPress REST API: praktyczne przypadki użycia dla custom post types
Jak używać WordPress REST API z custom post types w produkcji. Konkretne przykłady: headless CMS, mobile app backend, integracje z AI. Kod PHP + JS gotowy do użycia.
WordPress REST API to niedoceniane narzędzie. Wiele osób traktuje WP jako “tylko CMS do blogów”, a tymczasem pod spodem jest w pełni funkcjonalny backend z uwierzytelnianiem, uprawnieniami, custom post types i polami meta. Ten post to konkretne use case’y, nie sucha dokumentacja.
Custom post type z REST API support
Pierwszy krok: przy rejestracji CPT dodaj 'show_in_rest' => true i
'rest_base' => 'projekty':
// functions.php lub własny pluginadd_action('init', function () { register_post_type('projekt', [ 'labels' => [ 'name' => 'Projekty', 'singular_name' => 'Projekt', ], 'public' => true, 'show_in_rest' => true, // KLUCZ: eksponuje w REST API 'rest_base' => 'projekty', // /wp/v2/projekty 'rest_controller_class' => 'WP_REST_Posts_Controller', 'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'], 'has_archive' => true, 'menu_icon' => 'dashicons-portfolio', 'rewrite' => ['slug' => 'projekty'], ]);});Od teraz endpoint /wp-json/wp/v2/projekty zwraca tablicę projektów
jako JSON. Każdy projekt to pełny post object z polami: id, title
(rendered + raw), content, excerpt, featured_media, meta.
Dodawanie custom fields do REST API
Domyślnie custom fields (post meta) nie są widoczne w REST. Trzeba je jawnie zarejestrować:
// Rejestracja meta box + REST exposureadd_action('init', function () { register_post_meta('projekt', 'klient', [ 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => 'sanitize_text_field', 'auth_callback' => function () { return current_user_can('edit_posts'); }, ]);
register_post_meta('projekt', 'budzet', [ 'type' => 'number', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => 'absint', ]);
register_post_meta('projekt', 'data_realizacji', [ 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'sanitize_callback' => function ($value) { // ISO 8601 date validation $d = DateTime::createFromFormat('Y-m-d', $value); return $d && $d->format('Y-m-d') === $value ? $value : ''; }, ]);});Trzy rzeczy do zapamiętania:
sanitize_callback— zawsze. Bez niego user może wpisać cokolwiek w pole meta (XSS, SQL injection w edge case’ach).auth_callback— kto może edytować to pole. Domyślnie nikt. Zwracatruegdy user ma uprawnienia.- Walidacja formatu — dla dat, URL-i, JSON-ów. Lepiej zwrócić
pusty string niż zaakceptować
2025-13-99.
Custom endpoint z walidacją i uprawnieniami
Czasem potrzebujesz endpointu, który nie mapuje 1:1 na CPT. Np. “daj mi statystyki projektu”:
add_action('rest_api_init', function () { register_rest_route('custom/v1', '/projekt/(?P<id>\d+)/stats', [ 'methods' => 'GET', 'callback' => 'get_projekt_stats', 'permission_callback' => function () { return current_user_can('read'); }, 'args' => [ 'id' => [ 'validate_callback' => function ($param) { return is_numeric($param); }, ], ], ]);});
function get_projekt_stats(WP_REST_Request $request) { $projekt_id = absint($request['id']);
$projekt = get_post($projekt_id); if (!$projekt || $projekt->post_type !== 'projekt') { return new WP_Error('not_found', 'Projekt nie istnieje', ['status' => 404]); }
// Cache'owanie na 1h $cache_key = "projekt_stats_{$projekt_id}"; $stats = wp_cache_get($cache_key); if ($stats === false) { $stats = [ 'views' => (int) get_post_meta($projekt_id, 'views', true), 'leads' => (int) get_post_meta($projekt_id, 'leads', true), 'conversion_rate' => $this->calc_conversion($projekt_id), 'tasks_total' => $this->count_tasks($projekt_id), 'tasks_done' => $this->count_tasks($projekt_id, 'done'), ]; wp_cache_set($cache_key, $stats, '', HOUR_IN_SECONDS); }
return new WP_REST_Response($stats, 200);}Kluczowe punkty:
-
permission_callback— oddzielnie odcallback. WordPress sprawdza uprawnienia PRZED wywołaniem logiki. Bez tego każdy może odpytać endpoint. -
validate_callbackw args — walidacja URL parametrów. Bez tego ktoś może wpisać/projekt/abc/statsi dostać 500 error. -
Cache —
wp_cache_get+wp_cache_set. WordPress używa object cache (domyślnie transient, z Redis/Memcached — szybko). -
WP_Errorz status code — nigdy nie zwracajnullani pustej tablicy. Zwraca sensowne błędy z kodami HTTP (404, 403, 500).
Dodawanie computed fields do standardowych endpointów
Czasem chcesz rozszerzyć istniejący endpoint o dodatkowe pole,
np. “related_projects” w odpowiedzi /wp/v2/projekty:
add_action('rest_api_init', function () { register_rest_field('projekt', 'related_projects', [ 'get_callback' => 'get_related_projects', 'update_callback' => null, // read-only 'schema' => [ 'description' => 'Lista 3 powiązanych projektów', 'type' => 'array', 'items' => ['type' => 'object'], ], ]);});
function get_related_projects($post) { $tags = wp_get_post_tags($post['id'], ['fields' => 'ids']); if (empty($tags)) return [];
$related = get_posts([ 'post_type' => 'projekt', 'post__not_in' => [$post['id']], 'posts_per_page' => 3, 'tag__in' => $tags, 'fields' => 'ids', ]);
return array_map(function ($id) { $p = get_post($id); return [ 'id' => $id, 'title' => $p->post_title, 'permalink' => get_permalink($id), 'klient' => get_post_meta($id, 'klient', true), ]; }, $related);}Teraz każde wywołanie /wp/v2/projekty/123 zwraca dodatkowe pole
related_projects w JSON. Frontend może je wyświetlić bez dodatkowych
requestów.
Uwierzytelnianie dla zewnętrznych integracji
Dla system-to-system (np. Next.js frontend → WP backend) nie używaj cookie auth. Użyj Application Passwords (WP 5.6+):
# Generowanie hasła w WP Admin: Users → Profile → Application Passwords# WordPress daje 24-znakowe hasło, np. "abcd EFGH ijkl MNOP qrst UVWX"
# Request z autoryzacjącurl -u "admin:abcd EFGH ijkl MNOP qrst UVWX" \ https://example.com/wp-json/wp/v2/projektyW Node.js / Next.js:
const username = process.env.WP_USERNAME!;const appPassword = process.env.WP_APP_PASSWORD!;const baseUrl = process.env.WP_BASE_URL!;
const auth = Buffer.from(`${username}:${appPassword}`).toString('base64');
async function fetchProjects() { const res = await fetch(`${baseUrl}/wp-json/wp/v2/projekty?per_page=100`, { headers: { Authorization: `Basic ${auth}` }, next: { revalidate: 60 }, // ISR w Next.js }); return res.json();}Trzy warstwy bezpieczeństwa, które wdrażam w każdym projekcie:
- HTTPS only — bez tego hasło leci plain text.
- Application Password scope — hasło per integracja, nie główne. Jeśli wycieknie — unieważniasz jedno, nie wszystkie.
- Rate limiting w Cloudflare — patrz poprzedni post o Cloudflare. Bez tego boty mogą wyciągnąć wszystkie dane.
Use case 1: Headless CMS dla Next.js
WordPress jako backend, Next.js jako frontend. Pełna kontrola nad UX, SSG dla SEO, React dla interaktywności:
import { headers } from 'next/headers';
async function getProjects() { const auth = Buffer.from( `${process.env.WP_USERNAME}:${process.env.WP_APP_PASSWORD}` ).toString('base64');
const res = await fetch( `${process.env.WP_BASE_URL}/wp-json/wp/v2/projekty?_embed&per_page=50`, { headers: { Authorization: `Basic ${auth}` }, next: { revalidate: 300 }, // regeneruj co 5 min } );
if (!res.ok) throw new Error('Failed to fetch projects'); return res.json();}
export default async function ProjektyPage() { const projects = await getProjects();
return ( <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> {projects.map((p) => ( <article key={p.id} className="border rounded p-4"> <h2 dangerouslySetInnerHTML={{ __html: p.title.rendered }} /> <div dangerouslySetInnerHTML={{ __html: p.excerpt.rendered }} /> <span className="text-sm text-gray-500">{p.klient}</span> </article> ))} </div> );}_embed w URL zwraca powiązane zasoby w jednym requeście:
featured image (pełne URL-e w różnych rozmiarach), author (z avatar),
terms (tagi, kategorie). Bez _embed musiałbyś robić N dodatkowych
requestów na featured_media każdego projektu.
Use case 2: AI integration - generowanie opisu projektu
Mój workflow dla klientów: AI generuje draft opisu, redaktor
akceptuje. Endpoint POST /custom/v1/projekt/<id>/ai-description:
add_action('rest_api_init', function () { register_rest_route('custom/v1', '/projekt/(?P<id>\d+)/ai-description', [ 'methods' => 'POST', 'callback' => 'generate_ai_description', 'permission_callback' => function () { return current_user_can('edit_posts'); }, ]);});
function generate_ai_description(WP_REST_Request $request) { $projekt_id = absint($request['id']); $projekt = get_post($projekt_id); if (!$projekt) { return new WP_Error('not_found', 'Projekt nie istnieje', ['status' => 404]); }
$klient = get_post_meta($projekt_id, 'klient', true); $budzet = get_post_meta($projekt_id, 'budzet', true);
$prompt = "Napisz 3-zdaniowy opis projektu IT w języku polskim.\n"; $prompt .= "Klient: $klient\n"; $prompt .= "Budżet: $budzet PLN\n"; $prompt .= "Tytuł: {$projekt->post_title}\n"; $prompt .= "Bieżący opis: {$projekt->post_excerpt}\n";
$response = openai_chat_completion([ 'model' => 'gpt-4o-mini', 'messages' => [ ['role' => 'system', 'content' => 'Jesteś redaktorem opisu projektów IT.'], ['role' => 'user', 'content' => $prompt], ], 'temperature' => 0.7, ]);
if (is_wp_error($response)) { return $response; }
// Zapisz do custom field, NIE nadpisuj post_content update_post_meta($projekt_id, 'ai_description_draft', $response['content']);
return new WP_REST_Response([ 'draft' => $response['content'], 'cost_usd' => $response['cost'], ], 200);}Taki endpoint daje redaktorowi draft do zaakceptowania, zamiast
nadpisywać treść od razu. Bezpieczeństwo: tylko user z uprawnieniem
edit_posts może wywołać.
Use case 3: Mobile app backend
Prosta app mobilna (React Native) dla zespołu — pobiera aktywne projekty, pozwala dodać task. Endpointy z autoryzacją:
add_action('rest_api_init', function () { register_rest_route('mobile/v1', '/projekty', [ 'methods' => 'GET', 'callback' => 'mobile_get_projekty', 'permission_callback' => 'mobile_check_token', ]);
register_rest_route('mobile/v1', '/projekty/(?P<id>\d+)/tasks', [ 'methods' => 'POST', 'callback' => 'mobile_create_task', 'permission_callback' => 'mobile_check_token', ]);});
function mobile_check_token(WP_REST_Request $request) { $token = $request->get_header('X-Mobile-Token'); if (!$token) return new WP_Error('no_token', 'Brak tokenu', ['status' => 401]);
// HMAC: token = HMAC(secret, timestamp) $timestamp = $request->get_header('X-Mobile-Timestamp'); $expected = hash_hmac('sha256', $timestamp, MOBILE_SECRET); if (!hash_equals($expected, $token)) { return new WP_Error('invalid_token', 'Nieprawidłowy token', ['status' => 401]); } return true;}HMAC w headerze: app wysyła X-Mobile-Token (HMAC z secret) i
X-Mobile-Timestamp (Unix time). Backend weryfikuje HMAC, sprawdza
timestamp (max 5 min różnicy żeby uniknąć replay attacks). Prostsze
niż OAuth, bezpieczniejsze niż API key w URL.
Debugowanie i testowanie
# Test endpointu z autoryzacjącurl -u "admin:abcd EFGH ijkl MNOP qrst UVWX" \ https://example.com/wp-json/wp/v2/projekty?per_page=5
# Sprawdź dostępne endpointycurl https://example.com/wp-json/ | jq
# Sprawdź custom fields w odpowiedzicurl https://example.com/wp-json/wp/v2/projekty/123 | jq '.klient, .budzet'
# Sprawdź response headers (rate limit, cache)curl -I https://example.com/wp-json/wp/v2/projektyPlugin Query Monitor pokazuje w adminie WP: ile query SQL leci na request, ile czasu, które endpointy są wolne. Bez niego optymalizujesz na ślepo.
Co dalej
- Cloudflare dla WordPressa — pełna optymalizacja
- Wielojęzyczny WordPress z Polylang
- Multi-agent systemy AI w praktyce (integracja AI z WP przez REST)
Jeśli masz projekt WordPress, który chcesz przenieść na headless architekturę albo rozbudować o AI features — napisz do mnie. Realizacja: 2-4 tygodnie, koszt od 4000 PLN.
Najczęściej zadawane pytania
Czy WordPress REST API jest stabilny w produkcji?
Headless WordPress czy klasyczny WordPress z AJAX?
Jak zabezpieczyć custom endpoint przed nieautoryzowanym dostępem?
Czy mogę zwrócić related posts w jednym requeście?
Powiązane posty
- devops
Cloudflare dla WordPressa: pełna optymalizacja wydajności i bezpieczeństwa
Jak skonfigurować Cloudflare (Free/Pro) dla WordPressa krok po kroku: cache, APO, WAF, Bot Fight Mode, page rules, CDN. Realne wyniki pomiarów i konfiguracja dla Polski.
7 min - wordpress
Wielojęzyczny WordPress z Polylang: praktyczny przewodnik implementacji
Jak wdrożyć wielojęzyczność w WordPress z Polylang: konfiguracja, SEO, hreflang, tłumaczenia automatyczne, integracja z ACF i WooCommerce. Realne pułapki, kod, ustawienia.
7 min