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: praktyczne przypadki użycia dla custom post types
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 plugin
add_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 exposure
add_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/projekty
W 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:
// app/projekty/page.tsx
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 endpointy
curl https://example.com/wp-json/ | jq
# Sprawdź custom fields w odpowiedzi
curl 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/projekty
Plugin 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