Author Events API
Production-ready WordPress REST API plugin for managing author events.
by Sarfaraz Kazi · github.com/sarfaraz-kazi/crud-api-for-events · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/sarfaraz-kazi/crud-api-for-events/archive/refs/heads/main.zipA production-ready WordPress REST API plugin for managing author events. Built to enterprise standards: OOP/PHP 8.0+, a dedicated WP_REST_Controller subclass, JSON Schema validation, capability-gated permissions, and a full PHPUnit test suite.
Table of Contents
Features
- Custom Post Type
author_event— leverages the full WordPress data layer (meta API, revisions, trash/restore, capabilities) - Custom Taxonomy
event_category— hierarchical, admin-only - REST API under
author-events/v1/eventsextendingWP_REST_Controller - Full CRUD —
GET(collection + single),POST,PUT/PATCH,DELETE - Date filtering via
?date=YYYY-MM-DDon the collection endpoint - JSON Schema validation (
get_item_schema) — type coercion, required fields, and format checks are handled automatically per endpoint - Capability guards — every endpoint requires
manage_options; unauthenticated requests get401, authenticated non-admins get403 - HATEOAS links —
selfandcollectionlinks on every item response - Pagination headers —
X-WP-TotalandX-WP-TotalPageson collection responses - PSR-4 autoloader — no Composer dependency at runtime; switchable to Composer in one line
- 13-case PHPUnit test suite covering auth, CRUD, date filtering, DB verification, and response contract
Requirements
| Dependency | Minimum version |
|---|---|
| PHP | 8.0 |
| WordPress | 6.0 |
| PHPUnit (dev only) | 9.6 or 10.x |
Installation
Via upload
- Download or clone this repository.
- Copy the
author-events-api/folder intowp-content/plugins/. - Activate the plugin in Plugins → Installed Plugins.
Via Composer (dev environment)
git clone https://github.com/sarfaraz-kazi/CRUD-API-For-Events
composer install
No Composer autoload is required at runtime — the plugin ships its own spl_autoload_register loader.
Architecture
author-events-api/
├── author-events-api.php ← Entry point: constants + PSR-4 autoloader
├── composer.json
├── phpunit.xml
├── includes/
│ ├── class-plugin.php ← Singleton bootstrap (wires all hooks)
│ ├── class-cpt-registration.php ← CPT, taxonomy, and post meta registration
│ └── api/
│ └── class-events-controller.php ← WP_REST_Controller subclass
└── tests/
├── bootstrap.php
└── class-events-controller-test.php
Design decisions
WP_REST_Controller subclass, not procedural callbacks
get_endpoint_args_for_item_schema() reads the JSON Schema once and auto-generates the args array for every route — type coercion, sanitisation, and required validation included. filter_response_by_context() strips fields per context (view / edit / embed) without any per-endpoint logic.
CPT storage over a custom table
The WordPress data layer provides meta API, WP_Query with meta_query / tax_query, revisions, trash/restore, and capability maps out of the box. A custom table is warranted only when query patterns outgrow what WP_Query can express efficiently — unnecessary complexity for an MVP.
Separate entry point and Plugin class
author-events-api.php only defines constants and registers the autoloader. All hook wiring lives in Plugin::get_instance(), so every component can be instantiated in isolation during tests without bootstrapping the whole plugin.
CHAR type in meta_query BETWEEN
Dates are stored as ISO 8601 (YYYY-MM-DDTHH:MM:SS). MySQL's DATETIME type expects a space separator; CHAR keeps the comparison lexicographic, which is correct because ISO 8601 dates sort identically to their chronological order when zero-padded.
API Reference
Authentication
All endpoints require the current user to have the manage_options capability (WordPress Administrators by default). Authenticate using WordPress Application Passwords or cookie-based auth with a valid nonce.
Authorization: Basic base64(username:application-password)
| Condition | HTTP status |
|---|---|
| Not logged in | 401 Unauthorized |
| Logged in, insufficient capability | 403 Forbidden |
GET /events
Returns a paginated list of events, ordered by start_date ascending.
Query parameters
| Parameter | Type | Description |
|---|---|---|
date |
string (YYYY-MM-DD) |
Filter events whose start_date falls on this day |
category |
string |
Filter by event_category slug |
page |
integer |
Page number (default 1) |
per_page |
integer |
Items per page (default 10, max 100) |
Example request
curl -u admin:app-password \
"https://example.com/wp-json/author-events/v1/events?date=2025-06-15&per_page=5"
Example response 200 OK
[
{
"id": 42,
"title": "Book Launch: The Digital Age",
"description": "<p>Join us for the official launch...</p>",
"category": "book-launch",
"start_date": "2025-06-15T09:00:00",
"end_date": "2025-06-15T11:00:00",
"status": "publish",
"date_created": "2025-05-01T12:00:00",
"date_modified": "2025-05-10T08:30:00",
"_links": {
"self": [{ "href": "https://example.com/wp-json/author-events/v1/events/42" }],
"collection": [{ "href": "https://example.com/wp-json/author-events/v1/events" }]
}
}
]
Response headers
X-WP-Total: 12
X-WP-TotalPages: 3
GET /events/{id}
Returns a single event by its WordPress post ID.
curl -u admin:app-password \
"https://example.com/wp-json/author-events/v1/events/42"
| Status | Meaning |
|---|---|
200 OK |
Event found |
404 Not Found |
No event with that ID |
410 Gone |
Event exists but is in the trash |
POST /events
Creates a new event.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
title |
string |
Yes | Event title (1–200 characters) |
description |
string |
No | Full HTML description |
category |
string |
No | Category name — created automatically if it does not exist |
start_date |
string |
Yes | ISO 8601 datetime (YYYY-MM-DDTHH:MM:SS) |
end_date |
string |
Yes | ISO 8601 datetime — must be ≥ start_date |
status |
string |
No | publish (default), draft, pending, private |
Example request
curl -u admin:app-password \
-X POST \
-H "Content-Type: application/json" \
-d '{
"title": "Annual Author Summit",
"description": "<p>Gathering of published authors.</p>",
"category": "summit",
"start_date": "2025-09-10T09:00:00",
"end_date": "2025-09-10T17:00:00"
}' \
"https://example.com/wp-json/author-events/v1/events"
Response 201 Created — returns the created event with a Location header pointing to the new resource.
PUT/PATCH /events/{id}
Updates an existing event. PUT expects all writeable fields; PATCH accepts any subset.
curl -u admin:app-password \
-X PATCH \
-H "Content-Type: application/json" \
-d '{ "title": "Updated Title", "start_date": "2025-09-10T10:00:00", "end_date": "2025-09-10T18:00:00" }' \
"https://example.com/wp-json/author-events/v1/events/42"
Response 200 OK — returns the full updated event object.
DELETE /events/{id}
Moves the event to the WordPress trash by default. Pass ?force=true to permanently delete.
# Move to trash
curl -u admin:app-password -X DELETE \
"https://example.com/wp-json/author-events/v1/events/42"
# Permanent delete
curl -u admin:app-password -X DELETE \
"https://example.com/wp-json/author-events/v1/events/42?force=true"
Response 200 OK
{
"deleted": true,
"previous": { "id": 42, "title": "Annual Author Summit", "..." : "..." }
}
Response Schema
Every event item conforms to the following shape. Fields marked readonly are never accepted on write.
| Field | Type | Contexts | Notes |
|---|---|---|---|
id |
integer |
view, edit, embed | readonly |
title |
string |
view, edit, embed | 1–200 chars, required on create |
description |
string |
view, edit | HTML allowed (wp_kses_post) |
category |
string |
view, edit, embed | Term name; auto-created |
start_date |
string (ISO 8601) |
view, edit, embed | required on create |
end_date |
string (ISO 8601) |
view, edit, embed | required on create; must be ≥ start_date |
status |
string enum |
view, edit | publish | draft | pending | private |
date_created |
string (ISO 8601) |
view, edit | readonly |
date_modified |
string (ISO 8601) |
view, edit | readonly |
Error Responses
All errors follow the standard WordPress REST error envelope:
{
"code": "rest_invalid_param",
"message": "end_date must be on or after start_date.",
"data": { "status": 400 }
}
| Code | HTTP status | Trigger |
|---|---|---|
rest_forbidden |
401 / 403 | Missing or insufficient capability |
rest_invalid_param |
400 | Schema validation failure, bad date format, or end before start |
rest_event_not_found |
404 | Post ID does not exist or wrong post type |
rest_event_trashed |
410 | Post exists but is in trash |
rest_cannot_delete |
500 | wp_delete_post() returned false |
Running Tests
1. Install the WordPress test library
# Creates a test DB and downloads the WP test suite into /tmp/wordpress-tests-lib
bash bin/install-wp-tests.sh wordpress_test root '' localhost latest
2. Install dev dependencies
composer install
3. Run the suite
WP_TESTS_DIR=/tmp/wordpress-tests-lib vendor/bin/phpunit
With coverage report:
WP_TESTS_DIR=/tmp/wordpress-tests-lib vendor/bin/phpunit --coverage-html=coverage/
Test groups
| Group | Cases | What is verified |
|---|---|---|
| Auth & authorisation | 4 | Anonymous and subscriber requests return 401/403 on all write endpoints |
| POST + DB verification | 3 | Admin creates event → HTTP 201, correct response shape, correct DB row, correct meta, correct taxonomy term |
| Date filter | 4 | ?date= returns only matching events, empty result for no-match, 400 for invalid format |
| Full CRUD | 3 | GET single, PUT update (with DB re-read), DELETE with force (with DB re-read) |
| Response contract | 3 | Pagination headers present, HATEOAS links present, required schema fields present |
Roadmap
| Priority | Feature |
|---|---|
| High | Object cache — wrap WP_Query results with wp_cache_get/set keyed on a query hash |
| High | Audit log — record every API write (who, what, when) to a custom log CPT |
| Medium | WP-CLI commands — wp events create, wp events list --date=, wp events delete |
| Medium | GitHub Actions CI — matrix test across PHP 8.0–8.3 × WP 6.0–latest; PHPCS on every PR |
| Medium | Rate limiting — per-IP / per-token request throttle via rest_pre_dispatch, returns 429 |
| Low | Webhook dispatch — fire registered consumer URLs on create / update / delete |
| Low | Cursor-based pagination — replace OFFSET paging with a keyed start_date cursor for large datasets |
| Low | Import / Export — wp events import events.csv and wp events export --format=json |
License
GPL v2 or later — see LICENSE.