WordPress

Building a simple REST-style endpoint inside a WordPress plugin

WP

A client wanted a small "latest projects" widget that could be dropped into a static HTML sidebar on a partner site. Rather than exporting a static file by hand every time content changed, we exposed a tiny JSON endpoint straight out of WordPress.

The problem with the obvious options

The obvious first idea was to just have someone on the partner site's team manually copy over a chunk of HTML whenever the client published a new project. That lasted about a week before it was clearly not going to happen reliably, updates would lag by weeks and nobody wanted to own the job of remembering. The second idea, generating a static JSON file on every save and having them fetch that, was closer but meant syncing a file across two separate hosting environments, which is its own maintenance headache.

What we actually needed was much simpler than either: a URL the partner site could hit with jQuery.getJSON that always reflected current data, read-only, no authentication complexity, nothing fancy.

The approach

We added a custom query var and a rewrite rule so a request to /wp-json-lite/projects/ gets caught before WordPress tries to resolve it as a page or a 404. From there it's a normal WP_Query, looped, and pushed through json_encode().

  • add_rewrite_rule() plus flush_rewrite_rules() on activation only, never on every load, since flushing rewrite rules on every request is a well-documented way to tank performance on a busy site.
  • Set the Content-Type header to application/json manually with header(), since WordPress won't do it for you on a custom endpoint like this.
  • Call exit; after echoing the JSON so WordPress doesn't try to render a template underneath it, which without the exit call would append a whole HTML page after your JSON and break every consumer.

It's not a real REST API by any stretch, no proper routing, no content negotiation, no auth beyond an API key in the query string, but for a single read-only widget it did the job in under an hour and the partner site just pulls it in with a jQuery.getJSON call.

What actually goes in the response

We kept the payload deliberately small: project title, a short excerpt, a thumbnail URL, and a permalink back to the full case study on our client's own site. Nothing that needed escaping beyond what WordPress's own functions already handle, and nothing that exposed post IDs or any internal structure the partner site didn't need to see.

The rewrite rule itself gets registered on init, and the query var gets added through the query_vars filter so WordPress knows to preserve it. A template_redirect hook checks for that query var and short-circuits with our JSON output before WordPress goes looking for a page template that doesn't exist.

The API key, such as it is

Calling it authentication is generous. We check for a query string parameter matching a value stored in the plugin's options, and reject the request with a 403 status and a short JSON error body if it doesn't match. It stops a casual crawler from scraping the endpoint and it stops search engines from indexing it as a duplicate content source, but anyone who actually wanted the data could just look at the partner site's own source and copy the key out of the request URL.

For a single read-only feed of project titles and thumbnails, that's a risk we're comfortable with. We wouldn't ship anything remotely like this for data that mattered, pricing information, customer data, anything the client would be unhappy to see scraped.

Where this breaks down

A few things we've already flagged as limits of this approach rather than problems to fix right now:

  • No pagination. If the client's project list grows past a couple dozen entries, we'll need to add a page or offset parameter, but at eight projects it's not worth building yet.
  • No caching layer of any kind. Every request re-runs the WP_Query. Fine at low traffic, would need attention if the partner site's own traffic grew meaningfully.
  • No versioning story. If we ever need to change the response shape, there's no way to do that without breaking whatever's already consuming it, which for now is exactly one page on one partner site, so it's a manageable risk.

Would we build it this way again

For this specific ask, yes, without much hesitation. Building a full plugin-based REST API with proper routing, a request/response abstraction, real authentication, would have taken days instead of an hour, for a use case that genuinely doesn't need any of that sophistication. Over-engineering a single-purpose internal feed just because "REST API" sounds like the professional answer would have cost the client money for nothing they'd notice.

That said, this only works because we know exactly how limited the use case is and we're the ones maintaining both the endpoint and its one consumer. If this were public-facing, consumed by parties we don't control, or handling anything remotely sensitive, we'd reach for a proper structure from day one rather than growing into one later. The lesson we're taking away is less "always build the minimal thing" and more "match the amount of structure to how much you actually know about who's going to depend on this, and how badly it'll hurt to change."

We'll probably revisit this again in a year if the client's project catalog grows or if a second partner site wants to consume the same feed, at which point some of the shortcuts above, pagination especially, stop being acceptable. For now, a couple dozen lines of plugin code beats reaching for infrastructure we don't need yet.

Testing it before handing it off

Before telling the partner site's developer the endpoint was ready, we ran through a short list of manual checks rather than assuming the happy path was the whole story. Hitting the URL with the API key omitted confirmed the 403 response actually fired instead of silently returning data. Hitting it with an obviously wrong key did the same. We also checked what happened when the client's project list was completely empty, which returned a valid but empty JSON array rather than an error, exactly the behavior the partner site's jQuery code was written to expect.

None of that testing took more than fifteen minutes, but it's the fifteen minutes that turns "seems to work when I tried it" into something we're comfortable handing to someone else's codebase to depend on.

Documentation we left behind

Since another developer, one we don't work with directly, is the one actually consuming this endpoint, we wrote a short plain-text file describing the URL, the required key parameter, and the exact shape of the JSON response, including a sample response with real-looking data. It's maybe half a page, nothing fancy, but it meant the partner site's developer could build against it without a back-and-forth email chain asking what fields are available.

We've started doing this for any endpoint or integration point another team depends on, even an internal one between two of our own client projects. A five-minute writeup at handoff has consistently saved much longer email exchanges weeks later when someone forgets the exact parameter name.

A caching idea we considered and skipped

We briefly discussed wrapping the WP_Query result in a transient, WordPress's built-in caching mechanism, so repeated requests within a short window wouldn't hit the database at all. For a widget refreshed by exactly one partner site, at whatever polling interval their jQuery code uses, the actual database load this endpoint generates is negligible, a handful of requests a day at most. Adding a transient layer would have meant handling cache invalidation whenever the client publishes or edits a project, which is more moving parts than the traffic level justifies. We wrote it down as a note for later rather than building it now, since premature caching has its own failure mode: stale data serving quietly until someone notices the widget is showing last month's projects.

We've pinned a note to revisit this document alongside the endpoint itself if the partner integration ever grows into something with real business logic attached to it, rather than letting the two drift out of sync silently. For a first pass at exposing structured data out of WordPress without a framework in the way, it's held up better than we expected, and it's already the template we've reused for two smaller, similar requests from other clients since.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients