# REST API Skill

> **Purpose:** WordPress REST API patterns, endpoint registration, and API security
> **When to use:** Any task involving REST API endpoints
> **Related skills:** wordpress-plugin-foundation-skill.md (always applies)

---

## Quick Reference

```php
// Register endpoint
register_rest_route('presszone-forum/v1', '/posts', [
    'methods' => WP_REST_Server::READABLE,
    'callback' => [$this, 'getPosts'],
    'permission_callback' => [$this, 'checkPermission'],
    'args' => [
        'thread_id' => [
            'required' => true,
            'type' => 'integer',
            'sanitize_callback' => 'absint',
            'validate_callback' => function($value) {
                return is_numeric($value) && $value > 0;
            },
        ],
    ],
]);

// Success response
return new WP_REST_Response([
    'success' => true,
    'data' => $posts,
], 200);

// Error response
return new WP_Error(
    'not_found',
    __('Post not found', 'forum-press-zone'),
    ['status' => 404]
);
```

---

## Endpoint Registration

### Basic Structure

```php
class RestApi extends WP_REST_Controller
{
    public function registerRoutes(): void
    {
        register_rest_route('presszone-forum/v1', '/posts', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'getPosts'],
                'permission_callback' => [$this, 'checkPermission'],
                'args' => $this->getPostsArgs(),
            ],
            [
                'methods' => WP_REST_Server::CREATABLE,
                'callback' => [$this, 'createPost'],
                'permission_callback' => [$this, 'checkLoggedIn'],
                'args' => $this->createPostArgs(),
            ],
        ]);
    }
}

// Register in plugin initialization
add_action('rest_api_init', [$restApi, 'registerRoutes']);
```

### HTTP Methods

```php
WP_REST_Server::READABLE   // GET
WP_REST_Server::CREATABLE  // POST
WP_REST_Server::EDITABLE   // POST, PUT, PATCH
WP_REST_Server::DELETABLE  // DELETE
WP_REST_Server::ALLMETHODS // All methods
```

---

## Permission Callbacks (CRITICAL)

### ALWAYS Required

```php
// FORBIDDEN - Missing permission callback
register_rest_route('presszone-forum/v1', '/settings', [
    'methods' => 'POST',
    'callback' => [$this, 'updateSettings'],
    // Missing permission_callback - VULNERABLE!
]);

// FORBIDDEN - Public write endpoint
register_rest_route('presszone-forum/v1', '/settings', [
    'methods' => 'POST',
    'callback' => [$this, 'updateSettings'],
    'permission_callback' => '__return_true',  // VULNERABLE!
]);

// CORRECT - Proper permission check
register_rest_route('presszone-forum/v1', '/settings', [
    'methods' => 'POST',
    'callback' => [$this, 'updateSettings'],
    'permission_callback' => [$this, 'checkAdminPermission'],
]);
```

### Common Permission Callbacks

```php
// Logged in user
public function checkLoggedIn(): bool|WP_Error
{
    if (!is_user_logged_in()) {
        return new WP_Error(
            'rest_forbidden',
            __('You must be logged in', 'forum-press-zone'),
            ['status' => 401]
        );
    }
    return true;
}

// Admin permission
public function checkAdminPermission(): bool|WP_Error
{
    if (!current_user_can('manage_options')) {
        return new WP_Error(
            'rest_forbidden',
            __('Access denied', 'forum-press-zone'),
            ['status' => 403]
        );
    }
    return true;
}

// Moderator permission
public function checkModeratorPermission(): bool|WP_Error
{
    if (!is_user_logged_in()) {
        return new WP_Error('rest_forbidden', 'Login required', ['status' => 401]);
    }
    
    if (!Roles::canModerate()) {
        return new WP_Error('rest_forbidden', 'Access denied', ['status' => 403]);
    }
    
    return true;
}

// Public read, authenticated write
public function checkWritePermission(WP_REST_Request $request): bool|WP_Error
{
    if ($request->get_method() === 'GET') {
        return true;  // Public read
    }
    
    return $this->checkLoggedIn();  // Authenticated write
}
```

---

## Request Arguments

### Argument Schema

```php
private function getPostsArgs(): array
{
    return [
        'thread_id' => [
            'required' => true,
            'type' => 'integer',
            'description' => 'Thread ID',
            'sanitize_callback' => 'absint',
            'validate_callback' => function($value) {
                return is_numeric($value) && $value > 0;
            },
        ],
        'page' => [
            'required' => false,
            'type' => 'integer',
            'default' => 1,
            'minimum' => 1,
            'sanitize_callback' => 'absint',
        ],
        'per_page' => [
            'required' => false,
            'type' => 'integer',
            'default' => 50,
            'minimum' => 1,
            'maximum' => 100,
            'sanitize_callback' => 'absint',
        ],
    ];
}
```

### Validation Callbacks

```php
// Integer validation
'validate_callback' => function($value) {
    return is_numeric($value) && $value > 0;
}

// String length validation
'validate_callback' => function($value) {
    return is_string($value) && strlen($value) >= 3 && strlen($value) <= 200;
}

// Enum validation
'validate_callback' => function($value) {
    return in_array($value, ['draft', 'published', 'archived'], true);
}

// Email validation
'validate_callback' => function($value) {
    return is_email($value);
}
```

### Sanitization Callbacks

```php
'sanitize_callback' => 'absint'                    // Integer
'sanitize_callback' => 'sanitize_text_field'       // Plain text
'sanitize_callback' => 'sanitize_textarea_field'   // Textarea
'sanitize_callback' => 'sanitize_email'            // Email
'sanitize_callback' => 'sanitize_key'              // Key/slug
'sanitize_callback' => 'esc_url_raw'               // URL
'sanitize_callback' => 'wp_kses_post'              // Rich HTML
```

---

## Request Handling

### Get Parameters

```php
public function getPosts(WP_REST_Request $request): WP_REST_Response|WP_Error
{
    // Get parameters (already sanitized/validated)
    $thread_id = $request->get_param('thread_id');
    $page = $request->get_param('page') ?? 1;
    $per_page = $request->get_param('per_page') ?? 50;
    
    // Get all parameters
    $params = $request->get_params();
    
    // Get query parameters only
    $query_params = $request->get_query_params();
    
    // Get body parameters only
    $body_params = $request->get_body_params();
    
    // Get JSON body
    $json_params = $request->get_json_params();
    
    // Process request...
}
```

### File Uploads

```php
public function uploadFile(WP_REST_Request $request): WP_REST_Response|WP_Error
{
    $files = $request->get_file_params();
    
    if (empty($files['file'])) {
        return new WP_Error('no_file', 'No file uploaded', ['status' => 400]);
    }
    
    // Use WordPress upload handler
    $file = wp_handle_upload($files['file'], [
        'test_form' => false,
        'mimes' => [
            'jpg|jpeg|jpe' => 'image/jpeg',
            'png' => 'image/png',
        ],
    ]);
    
    if (isset($file['error'])) {
        return new WP_Error('upload_failed', $file['error'], ['status' => 400]);
    }
    
    return new WP_REST_Response([
        'url' => $file['url'],
        'path' => $file['file'],
    ], 201);
}
```

---

## Response Handling

### Success Response

```php
// Simple success
return new WP_REST_Response([
    'success' => true,
    'data' => $posts,
], 200);

// Created resource
return new WP_REST_Response([
    'id' => $post_id,
    'message' => __('Post created', 'forum-press-zone'),
], 201);

// No content
return new WP_REST_Response(null, 204);
```

### Error Response

```php
// Not found
return new WP_Error(
    'not_found',
    __('Post not found', 'forum-press-zone'),
    ['status' => 404]
);

// Validation error
return new WP_Error(
    'invalid_data',
    __('Invalid post data', 'forum-press-zone'),
    ['status' => 400]
);

// Forbidden
return new WP_Error(
    'rest_forbidden',
    __('Access denied', 'forum-press-zone'),
    ['status' => 403]
);

// Server error
return new WP_Error(
    'server_error',
    __('An error occurred', 'forum-press-zone'),
    ['status' => 500]
);
```

### HTTP Status Codes

```php
200  // OK
201  // Created
204  // No Content
400  // Bad Request
401  // Unauthorized
403  // Forbidden
404  // Not Found
409  // Conflict
422  // Unprocessable Entity
429  // Too Many Requests
500  // Internal Server Error
```

---

## Pagination

### Paginated Response

```php
public function getPosts(WP_REST_Request $request): WP_REST_Response
{
    $page = $request->get_param('page') ?? 1;
    $per_page = $request->get_param('per_page') ?? 50;
    
    $result = $this->query->getPosts($thread_id, $page, $per_page);
    
    $response = new WP_REST_Response($result['posts'], 200);
    
    // Add pagination headers
    $response->header('X-WP-Total', $result['total']);
    $response->header('X-WP-TotalPages', $result['total_pages']);
    
    return $response;
}
```

---

## Authentication

### Nonce Authentication (Default)

```javascript
// Client-side - Include nonce in header
fetch('/wp-json/presszone-forum/v1/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-WP-Nonce': wpApiSettings.nonce  // WordPress provides this
    },
    credentials: 'same-origin',
    body: JSON.stringify(data)
});
```

### Application Passwords

```php
// Enable application passwords for REST API
add_filter('wp_is_application_passwords_available', '__return_true');

// Client uses Basic Auth with application password
// Authorization: Basic base64(username:app_password)
```

---

## Route Parameters

### Dynamic Route Segments

```php
// Register route with parameter
register_rest_route('presszone-forum/v1', '/posts/(?P<id>\d+)', [
    'methods' => WP_REST_Server::READABLE,
    'callback' => [$this, 'getPost'],
    'permission_callback' => '__return_true',
    'args' => [
        'id' => [
            'validate_callback' => function($value) {
                return is_numeric($value);
            },
        ],
    ],
]);

// Access parameter
public function getPost(WP_REST_Request $request): WP_REST_Response|WP_Error
{
    $post_id = $request->get_param('id');
    
    $post = $this->query->getPost($post_id);
    
    if (!$post) {
        return new WP_Error('not_found', 'Post not found', ['status' => 404]);
    }
    
    return new WP_REST_Response($post, 200);
}
```

---

## CORS Support

### Enable CORS

```php
add_action('rest_api_init', function() {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
    add_filter('rest_pre_serve_request', function($value) {
        header('Access-Control-Allow-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, X-WP-Nonce');
        return $value;
    });
}, 15);
```

---

## Rate Limiting

### Implement Rate Limiting

```php
public function checkRateLimit(WP_REST_Request $request): bool|WP_Error
{
    $user_id = get_current_user_id();
    $ip = $_SERVER['REMOTE_ADDR'];
    $key = $user_id ? "user_{$user_id}" : "ip_{$ip}";
    
    $cache_key = "rate_limit_{$key}";
    $requests = wp_cache_get($cache_key, 'presszone_forum_api') ?: 0;
    
    // Limit: 100 requests per hour
    if ($requests >= 100) {
        return new WP_Error(
            'rate_limit_exceeded',
            __('Too many requests. Please try again later.', 'forum-press-zone'),
            ['status' => 429]
        );
    }
    
    wp_cache_set($cache_key, $requests + 1, 'presszone_forum_api', 3600);
    
    return true;
}

// Use in permission callback
public function checkPermission(WP_REST_Request $request): bool|WP_Error
{
    $rate_limit = $this->checkRateLimit($request);
    if (is_wp_error($rate_limit)) {
        return $rate_limit;
    }
    
    return $this->checkLoggedIn();
}
```

---

## Response Formatting

### Consistent Response Structure

```php
// Success response helper
protected function respondSuccess($data, int $status = 200): WP_REST_Response
{
    return new WP_REST_Response([
        'success' => true,
        'data' => $data,
    ], $status);
}

// Error response helper
protected function respondError(
    string $code,
    string $message,
    int $status = 400
): WP_Error {
    return new WP_Error($code, $message, ['status' => $status]);
}

// Usage
public function createPost(WP_REST_Request $request): WP_REST_Response|WP_Error
{
    $data = $request->get_params();
    
    $post_id = $this->createPostInDatabase($data);
    
    if (!$post_id) {
        return $this->respondError(
            'creation_failed',
            __('Failed to create post', 'forum-press-zone'),
            500
        );
    }
    
    return $this->respondSuccess(['id' => $post_id], 201);
}
```

---

## Schema Definition

### Endpoint Schema

```php
public function getItemSchema(): array
{
    return [
        '$schema' => 'http://json-schema.org/draft-04/schema#',
        'title' => 'post',
        'type' => 'object',
        'properties' => [
            'id' => [
                'description' => 'Unique identifier for the post',
                'type' => 'integer',
                'context' => ['view', 'edit'],
                'readonly' => true,
            ],
            'title' => [
                'description' => 'Post title',
                'type' => 'string',
                'context' => ['view', 'edit'],
                'required' => true,
            ],
            'content' => [
                'description' => 'Post content',
                'type' => 'string',
                'context' => ['view', 'edit'],
            ],
            'author' => [
                'description' => 'Author user ID',
                'type' => 'integer',
                'context' => ['view', 'edit'],
            ],
        ],
    ];
}
```

---

## Common Patterns

### Resource CRUD

```php
class PostsController extends WP_REST_Controller
{
    // GET /posts
    public function getItems(WP_REST_Request $request): WP_REST_Response
    {
        $posts = $this->query->getPosts($request->get_params());
        return new WP_REST_Response($posts, 200);
    }
    
    // GET /posts/{id}
    public function getItem(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $post = $this->query->getPost($request->get_param('id'));
        
        if (!$post) {
            return new WP_Error('not_found', 'Post not found', ['status' => 404]);
        }
        
        return new WP_REST_Response($post, 200);
    }
    
    // POST /posts
    public function createItem(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $post_id = $this->query->createPost($request->get_params());
        
        if (!$post_id) {
            return new WP_Error('creation_failed', 'Failed to create', ['status' => 500]);
        }
        
        return new WP_REST_Response(['id' => $post_id], 201);
    }
    
    // PUT /posts/{id}
    public function updateItem(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $result = $this->query->updatePost(
            $request->get_param('id'),
            $request->get_params()
        );
        
        if (!$result) {
            return new WP_Error('update_failed', 'Failed to update', ['status' => 500]);
        }
        
        return new WP_REST_Response(['success' => true], 200);
    }
    
    // DELETE /posts/{id}
    public function deleteItem(WP_REST_Request $request): WP_REST_Response|WP_Error
    {
        $result = $this->query->deletePost($request->get_param('id'));
        
        if (!$result) {
            return new WP_Error('delete_failed', 'Failed to delete', ['status' => 500]);
        }
        
        return new WP_REST_Response(null, 204);
    }
}
```

---

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Missing `permission_callback` | Always specify permission callback |
| Using `__return_true` for write endpoints | Use proper permission check |
| Not sanitizing input | Use `sanitize_callback` in args |
| Not validating input | Use `validate_callback` in args |
| Returning array instead of WP_REST_Response | Wrap in `new WP_REST_Response()` |
| Not setting HTTP status code | Always specify status code |
| Not handling errors | Return `WP_Error` with status |
| Missing nonce in client requests | Include `X-WP-Nonce` header |
| Not checking `is_wp_error()` | Always check for errors |
| Hardcoding namespace | Use variable for API version |

---

## Integration with Other Skills

- **wordpress-plugin-foundation-skill.md** - Security and compliance (always applies)
- **php-skill.md** - PHP patterns and error handling
- **sql-skill.md** - Database queries in callbacks
- **javascript-skill.md** - Client-side API requests
