Building Custom REST APIs in WordPress: A Developer's Guide
WordPress REST API has fundamentally changed how developers build with WordPress. Instead of just serving web pages, you can now use WordPress as a powerful backend system powering multiple frontends — mobile apps, decoupled JavaScript frameworks, native applications, and more.
Whether you're building a headless WordPress implementation or adding API capabilities to existing sites, understanding custom endpoints is essential for modern WordPress development.
Why Custom REST Endpoints Matter
The default WordPress REST API exposes posts, users, and basic resources. But real-world applications need custom functionality:
- E-commerce: Custom product filtering, inventory checks, order processing
- Booking systems: Availability lookups, appointment creation, calendar sync
- SaaS platforms: User analytics, subscription management, custom workflows
- Mobile apps: Lightweight endpoints optimized for mobile performance
- Decoupled frontends: Next.js, React, Vue.js frontends powered by WordPress
Custom endpoints let you expose exactly what clients need, nothing more.
Understanding REST API Basics
Before creating custom endpoints, understand REST principles:
REST concepts:
- Resources: Posts, users, products (represented by URLs)
- HTTP methods: GET (read), POST (create), PUT/PATCH (update), DELETE (remove)
- Status codes: 200 (success), 404 (not found), 403 (forbidden), 500 (error)
- Requests/responses: JSON format for data exchange
WordPress implementation:
- Base URL:
https://yoursite.com/wp-json/ - Default namespace:
wp/v2/ - Custom namespace:
myapp/v1/
Step 1: Create Your First Custom Endpoint
Let's build a simple endpoint that returns featured products:
// In your theme's functions.php or custom plugin
add_action('rest_api_init', function() {
register_rest_route('myapp/v1', '/featured-products', array(
'methods' => 'GET',
'callback' => 'get_featured_products',
'permission_callback' => '__return_true'
));
});
function get_featured_products() {
$args = array(
'post_type' => 'product',
'meta_key' => '_featured',
'meta_value' => 'yes',
'posts_per_page' => 10
);
$products = get_posts($args);
$response = array();
foreach ($products as $product) {
$response[] = array(
'id' => $product->ID,
'title' => $product->post_title,
'excerpt' => wp_trim_excerpt($product->post_content),
'image' => get_the_post_thumbnail_url($product->ID),
'price' => get_post_meta($product->ID, '_price', true)
);
}
return rest_ensure_response($response);
}
Testing your endpoint:
- Visit:
https://yoursite.com/wp-json/myapp/v1/featured-products - You should see JSON response with featured products
- Use Postman or your browser's dev tools to inspect
Step 2: Add Query Parameters
APIs need flexibility. Let's add filtering capabilities:
add_action('rest_api_init', function() {
register_rest_route('myapp/v1', '/products', array(
'methods' => 'GET',
'callback' => 'get_filtered_products',
'permission_callback' => '__return_true',
'args' => array(
'category' => array(
'description' => 'Filter by product category',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field'
),
'per_page' => array(
'description' => 'Results per page',
'type' => 'integer',
'default' => 10,
'sanitize_callback' => 'absint'
),
'page' => array(
'description' => 'Page number',
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint'
)
)
));
});
function get_filtered_products($request) {
$category = $request->get_param('category');
$per_page = $request->get_param('per_page');
$page = $request->get_param('page');
$args = array(
'post_type' => 'product',
'posts_per_page' => $per_page,
'paged' => $page
);
if ($category) {
$args['tax_query'] = array(
array(
'taxonomy' => 'product_cat',
'field' => 'slug',
'terms' => $category
)
);
}
$products = get_posts($args);
// ... format response
return rest_ensure_response($response);
}
Usage examples:
/wp-json/myapp/v1/products?per_page=20/wp-json/myapp/v1/products?category=electronics&page=2
Step 3: Handle POST Requests
Create endpoints for user-submitted data (forms, bookings, submissions):
add_action('rest_api_init', function() {
register_rest_route('myapp/v1', '/contact', array(
'methods' => 'POST',
'callback' => 'handle_contact_form',
'permission_callback' => '__return_true',
'args' => array(
'name' => array('required' => true),
'email' => array('required' => true),
'message' => array('required' => true)
)
));
});
function handle_contact_form($request) {
$name = sanitize_text_field($request->get_param('name'));
$email = sanitize_email($request->get_param('email'));
$message = sanitize_textarea_field($request->get_param('message'));
// Validate email
if (!is_email($email)) {
return new WP_Error('invalid_email', 'Invalid email address',
array('status' => 400));
}
// Send email or save to database
wp_mail(get_option('admin_email'), "New Contact: $name", $message);
return rest_ensure_response(array(
'success' => true,
'message' => 'Thank you! We received your message.'
));
}
Step 4: Implement Authentication
Protect sensitive endpoints from unauthorized access:
function get_user_dashboard($request) {
// Check if user is authenticated
if (!is_user_logged_in()) {
return new WP_Error('not_authenticated', 'User not authenticated',
array('status' => 401));
}
$user_id = get_current_user_id();
// Check user capability
if (!current_user_can('read_private_posts')) {
return new WP_Error('insufficient_permissions',
'You do not have permission to access this endpoint',
array('status' => 403));
}
// Return user-specific data
return rest_ensure_response(array(
'user_id' => $user_id,
'user_email' => get_userdata($user_id)->user_email,
'dashboard_data' => get_user_meta($user_id, 'dashboard', true)
));
}
Permission callback options:
'permission_callback' => '__return_true'- Public endpoint'permission_callback' => 'is_user_logged_in'- Logged-in users only'permission_callback' => function() { return current_user_can('manage_options'); }- Admins only
Step 5: Error Handling and Validation
Robust APIs handle errors gracefully:
function create_booking($request) {
$date = $request->get_param('date');
$time = $request->get_param('time');
$email = $request->get_param('email');
// Validate date format
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return new WP_Error('invalid_date',
'Date must be in YYYY-MM-DD format',
array('status' => 400));
}
// Check availability
$existing = get_posts(array(
'post_type' => 'booking',
'meta_query' => array(
array('key' => 'date', 'value' => $date),
array('key' => 'time', 'value' => $time)
)
));
if (!empty($existing)) {
return new WP_Error('slot_taken',
'This time slot is not available',
array('status' => 409));
}
// Create booking
$booking_id = wp_insert_post(array(
'post_type' => 'booking',
'post_status' => 'publish'
));
update_post_meta($booking_id, 'date', $date);
update_post_meta($booking_id, 'time', $time);
update_post_meta($booking_id, 'email', $email);
return rest_ensure_response(array(
'success' => true,
'booking_id' => $booking_id,
'message' => 'Booking created successfully'
));
}
Best Practices for Production APIs
Security:
- Always sanitize/validate input
- Use nonces for state-changing operations
- Implement rate limiting (WP Rate Limit plugin)
- Use HTTPS exclusively
- Implement CORS headers carefully
Performance:
- Cache expensive queries with
get_transient() - Use pagination (don't return thousands of items)
- Index database queries
- Test with Query Monitor
Documentation:
- Document all endpoints (URL, method, params, response)
- Provide example requests/responses
- Explain error codes
- Share API reference with clients
Versioning:
- Use versioned namespaces (
/myapp/v1/,/myapp/v2/) - Don't break v1 when adding v2
- Allow gradual client migration
Tools for API Development
- Postman: Test endpoints and build API documentation
- REST Client (VS Code): Quick testing from your editor
- WP REST API Console: Built-in WordPress documentation
- Query Monitor: Debug slow endpoints
- WP-CLI: Create and test endpoints from command line
Conclusion
Custom REST endpoints transform WordPress from a content platform into a flexible backend system. Whether you're building mobile apps, decoupled frontends, or third-party integrations, custom endpoints give you complete control.
Start simple (GET endpoint for data), then evolve toward complex operations (POST with validation, authentication, error handling). Your API will be more powerful, secure, and valuable.
What custom endpoints are you building? Share your use case in the comments — we'd love to hear about it.
