# REST API Development Skill

> **Technology:** WordPress REST API for backend endpoints

---

## Purpose

This skill covers REST endpoint creation, authentication, and response formatting for the Comments Press Zone plugin.

---

## REST API Architecture

### Namespace
`presszone-comments/v1`

### Base URL
`https://example.com/wp-json/presszone-comments/v1/`

---

## Endpoint Registration Pattern

```php
namespace CommentsPressZone\Api;

class RestAdmin {
    private string $namespace = 'presszone-comments/v1';
    
    public function register_routes(): void {
        // GET /dashboard/stats
        register_rest_route($this->namespace, '/dashboard/stats', [
            'methods' => 'GET',
            'callback' => [$this, 'get_stats'],
            'permission_callback' => [$this, 'check_permission'],
        ]);
        
        // POST /settings
        register_rest_route($this->namespace, '/settings', [
            [
                'methods' => 'GET',
                'callback' => [$this, 'get_settings'],
                'permission_callback' => [$this, 'check_permission'],
            ],
            [
                'methods' => 'POST',
                'callback' => [$this, 'update_settings'],
                'permission_callback' => [$this, 'check_permission'],
                'args' => $this->get_settings_schema(),
            ],
        ]);
    }
    
    public function check_permission(): bool {
        return current_user_can('manage_options') || current_user_can('moderate_comments');
    }
}
```

---

## Request Handling

### GET Request

```php
public function get_stats(\WP_REST_Request $request): \WP_REST_Response {
    // Access query parameters
    $period = $request->get_param('period') ?? '7days';
    
    // Fetch data
    $stats = $this->calculate_stats($period);
    
    return new \WP_REST_Response([
        'success' => true,
        'data' => $stats,
    ], 200);
}
```

### POST Request

```php
public function update_settings(\WP_REST_Request $request): \WP_REST_Response {
    // Get JSON body
    $settings = $request->get_json_params();
    
    // Sanitize
    $sanitized = [
        'enable_voting' => (bool) ($settings['enable_voting'] ?? true),
        'max_comment_length' => absint($settings['max_comment_length'] ?? 2000),
    ];
    
    // Save
    update_option('presszone_comments_settings', $sanitized);
    
    return new \WP_REST_Response([
        'success' => true,
        'message' => esc_html__('Settings saved.', 'comments-press-zone'),
    ], 200);
}
```

---

## Authentication

### X-WP-Nonce Header (Automatic)

```javascript
// Frontend
const response = await fetch('/wp-json/presszone-comments/v1/settings', {
    method: 'GET',
    headers: {
        'X-WP-Nonce': wpApiSettings.nonce // WordPress provides this
    },
    credentials: 'same-origin'
});
```

### Manual Nonce Verification (if needed)

```php
public function check_nonce(\WP_REST_Request $request): bool {
    $nonce = $request->get_header('X-WP-Nonce');
    return wp_verify_nonce($nonce, 'wp_rest');
}
```

---

## Request Validation (Schema)

```php
private function get_settings_schema(): array {
    return [
        'enable_voting' => [
            'type' => 'boolean',
            'required' => false,
            'default' => true,
            'sanitize_callback' => function($value) {
                return (bool) $value;
            },
        ],
        'max_comment_length' => [
            'type' => 'integer',
            'required' => false,
            'default' => 2000,
            'minimum' => 100,
            'maximum' => 10000,
            'sanitize_callback' => 'absint',
        ],
        'banned_words' => [
            'type' => 'string',
            'required' => false,
            'default' => '',
            'sanitize_callback' => 'sanitize_textarea_field',
        ],
    ];
}
```

---

## Response Formatting

### Success Response

```php
return new \WP_REST_Response([
    'success' => true,
    'data' => [
        'id' => 123,
        'name' => 'Example',
    ],
    'message' => esc_html__('Operation successful.', 'comments-press-zone'),
], 200);
```

### Error Response

```php
return new \WP_REST_Response([
    'success' => false,
    'message' => esc_html__('Invalid data.', 'comments-press-zone'),
    'code' => 'invalid_data',
], 400);
```

### WP_Error Response

```php
return new \WP_Error(
    'unauthorized',
    esc_html__('You do not have permission.', 'comments-press-zone'),
    ['status' => 403]
);
```

---

## HTTP Status Codes

| Code | Meaning | When to Use |
|------|---------|-------------|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST creating resource |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input data |
| 401 | Unauthorized | Not logged in |
| 403 | Forbidden | Logged in but no permission |
| 404 | Not Found | Resource doesn't exist |
| 500 | Internal Error | Server error |

---

## Route Patterns

### Resource Collection

```php
// GET /comments - List comments
register_rest_route($this->namespace, '/comments', [
    'methods' => 'GET',
    'callback' => [$this, 'get_comments'],
    'args' => [
        'page' => [
            'type' => 'integer',
            'default' => 1,
        ],
        'per_page' => [
            'type' => 'integer',
            'default' => 20,
        ],
    ],
]);

// POST /comments - Create comment
register_rest_route($this->namespace, '/comments', [
    'methods' => 'POST',
    'callback' => [$this, 'create_comment'],
]);
```

### Single Resource

```php
// GET /comments/{id} - Get single comment
register_rest_route($this->namespace, '/comments/(?P<id>\d+)', [
    'methods' => 'GET',
    'callback' => [$this, 'get_comment'],
    'args' => [
        'id' => [
            'validate_callback' => function($param) {
                return is_numeric($param);
            },
        ],
    ],
]);

// PUT /comments/{id} - Update comment
register_rest_route($this->namespace, '/comments/(?P<id>\d+)', [
    'methods' => 'PUT',
    'callback' => [$this, 'update_comment'],
]);

// DELETE /comments/{id} - Delete comment
register_rest_route($this->namespace, '/comments/(?P<id>\d+)', [
    'methods' => 'DELETE',
    'callback' => [$this, 'delete_comment'],
]);
```

---

## Pagination

```php
public function get_comments(\WP_REST_Request $request): \WP_REST_Response {
    $page = $request->get_param('page') ?? 1;
    $per_page = $request->get_param('per_page') ?? 20;
    $offset = ($page - 1) * $per_page;
    
    global $wpdb;
    $total = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->comments}");
    $comments = $wpdb->get_results($wpdb->prepare(
        "SELECT * FROM {$wpdb->comments} LIMIT %d OFFSET %d",
        $per_page,
        $offset
    ), ARRAY_A);
    
    $response = new \WP_REST_Response([
        'success' => true,
        'data' => $comments,
    ], 200);
    
    // Add pagination headers
    $response->header('X-WP-Total', $total);
    $response->header('X-WP-TotalPages', ceil($total / $per_page));
    
    return $response;
}
```

---

## Error Handling

```php
public function update_comment(\WP_REST_Request $request): \WP_REST_Response|\WP_Error {
    $comment_id = (int) $request->get_param('id');
    
    // Check if comment exists
    $comment = get_comment($comment_id);
    if (!$comment) {
        return new \WP_Error(
            'comment_not_found',
            esc_html__('Comment not found.', 'comments-press-zone'),
            ['status' => 404]
        );
    }
    
    // Check permission
    if (!current_user_can('edit_comment', $comment_id)) {
        return new \WP_Error(
            'unauthorized',
            esc_html__('You cannot edit this comment.', 'comments-press-zone'),
            ['status' => 403]
        );
    }
    
    // Process update
    try {
        $this->update_comment_data($comment_id, $request->get_json_params());
        
        return new \WP_REST_Response([
            'success' => true,
            'message' => esc_html__('Comment updated.', 'comments-press-zone'),
        ], 200);
    } catch (\Exception $e) {
        return new \WP_Error(
            'update_failed',
            esc_html__('Update failed.', 'comments-press-zone'),
            ['status' => 500]
        );
    }
}
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Missing `permission_callback` | ALWAYS specify (WordPress 5.5+) |
| Not sanitizing input | Use schema with `sanitize_callback` |
| Wrong HTTP status code | Use appropriate code for each response |
| Not using namespace | Always prefix with `presszone-comments/v1` |
| Forgetting to return WP_REST_Response | Return proper response object |
| Not handling WP_Error | Check for errors in responses |

---

## Testing Checklist

- [ ] All routes have `permission_callback`
- [ ] All input sanitized via schema
- [ ] All output escaped in messages
- [ ] Proper HTTP status codes used
- [ ] Nonce verification works (X-WP-Nonce header)
- [ ] Error responses are consistent
- [ ] Pagination implemented for lists
- [ ] Text domain in all messages
