Author Events API
Production-ready WordPress REST API plugin for managing author events.
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
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.