Skip to content

Ship a Custom WordPress Endpoint Without Writing a Plugin

Your JavaScript frontend needs a list of featured products from WordPress. Not the whole REST API surface, not raw post objects with forty fields, just the ten items the homepage carousel shows, shaped the way the frontend wants them.

The endpoint is ten minutes of work: query, map, return. The question that eats the rest of the afternoon: where does that code go?

functions.php. Fastest to write, tied to your theme. Switch themes, lose the endpoint. Two years later it’s line 1400 of a file nobody wants to scroll.

A site-specific plugin. The clean classic answer. But now your ten minutes of logic come wrapped in a plugin header, a hook, and a registration array:

add_action('rest_api_init', function () {
register_rest_route('acme/v1', '/featured-products', [
'methods' => 'GET',
'callback' => 'acme_featured_products',
'permission_callback' => '__return_true',
]);
});

And you still need a way to get that plugin onto the site. And onto staging. And to know which version runs where.

A snippets plugin. Deployed in seconds, versioned never. The code lives in the database, edited through an admin textarea, invisible to Git and code review.

The endpoint logic is never the problem. The delivery vehicle is.

With Loopress Full installed, a custom endpoint is a single PHP file in your repo:

<?php
declare(strict_types=1);
class FeaturedProducts
{
public function get(): array
{
$query = new WP_Query([
'post_type' => 'product',
'meta_key' => 'featured',
'meta_value' => '1',
'posts_per_page' => 10,
]);
return array_map(fn(WP_Post $post): array => [
'id' => $post->ID,
'title' => $post->post_title,
'url' => get_permalink($post),
], $query->posts);
}
}

Save it as api/featured-products.php and deploy:

Terminal window
lps api push

The conventions do all the wiring you saw above. The filename is the route. The class name is the filename in PascalCase. Each public method named after an HTTP verb (get, post, put, patch, delete) becomes that verb’s handler. No registration code, no plugin header, no hook.

Terminal window
curl https://your-site.com/wp-json/loopress-api/v1/featured-products
{"code":"rest_forbidden","message":"Sorry, you are not allowed to do that.","data":{"status":401}}

That 401 is the feature. A fresh route requires an administrator, the same authentication the Loopress CLI itself uses. Nothing you push is publicly reachable by accident.

For an endpoint another system calls on your behalf, a reporting script, an internal dashboard, a cron job, that default is already the right answer: the caller authenticates with an application password over Basic auth, and you have a protected endpoint without writing a single line of security code.

This feed, though, is meant for the browser. Making it public is a decision, so it’s expressed where decisions belong, in the file, in the diff, in code review:

public function permission(): callable
{
return fn(): bool => true;
}
Terminal window
curl https://your-site.com/wp-json/loopress-api/v1/featured-products
[{"id":231,"title":"Weekender Backpack","url":"https://your-site.com/product/weekender-backpack"}]

One greppable line. A reviewer seeing this diff knows exactly what’s being opened to the world, which is not something you can say about a checkbox in an admin screen. The same permission() method takes any callable, so routes that need their own rules, logged-in users only, a capability check, a signed webhook, express them the same way (the docs cover those patterns).

The frontend lives on another domain? Declare CORS headers in the file too, they apply to every request, preflight included:

public function headers(): array
{
return ['Access-Control-Allow-Origin' => 'https://app.example.com'];
}

Need an HTTP client or an SDK in a route? If the site uses Loopress’s Composer integration, your packages are directly available to use in route files. No manual autoloader loading.

Deployed code that runs inside WordPress has one non-negotiable requirement: a mistake must stay contained.

  • lps api push syntax-checks every file server-side and rejects broken PHP with the actual parse error, before anything is written.
  • A file that still fails at runtime (a class collision with another plugin, a throwing dependency) is skipped and logged. Every other route, and the rest of the site’s REST API, keeps working.
  • Files are protected from direct HTTP access and written atomically. Pull them back and you get exactly the source you wrote.

Because routes are plain files, everything you already do with code applies:

Terminal window
lps api pull # mirror what's on the site locally
git checkout -b add-feed # branch, edit, review
lps api push # deploy

Different environments? Push the same files to staging first, then production, like the rest of your Loopress-managed configuration. Endpoints stop being something you set up on a site and become something your repository declares.


Custom API Routes ship with Loopress Full, the free full edition of the plugin:

Terminal window
npm install -g @loopress/cli

The documentation covers the complete file reference: request handling, response types, authentication, CORS, namespaces, and the security model.