Custom Backend and API in WordPress

Building a Custom Backend and API with WordPress

David Welch JavaScript, Programming, Tutorial Leave a Comment

You love frontend JavaScript frameworks, but your client wants a WordPress website… Don’t worry, you can do both! Let’s build a custom backend and API with WordPress that can be consumed by your JavaScript framework of choice.

WordPress provides an out-of-the-box CMS solution that can be customized very easily to fit many different types of projects. You’re not just stuck with blog posts and web pages, either.

In this post, I’m going to show you how you can use a very popular plugin called Advanced Custom Fields (ACF) plus some custom code to create your own content and API endpoints for your fancy new frontend to consume while allowing your client to keep using what they love.

For this tutorial, I’m going to assume you have some familiarity with WordPress for content management and that you are familiar with PHP and APIs. To demonstrate, I’ll create a sample project, an application for an animal shelter.

We’ll build a very basic UI and API for our fictitious animal shelter where we will be able to add and update pets in WordPress and get that data from a custom API.

ACF Magic

The Advanced Custom Fields plugin makes building custom user interfaces quick and easy in WordPress. There are free and premium versions and I highly recommend the premium version as it adds some features that can be very helpful, like repeater fields.

You can use the free version for this tutorial. Simply install the plugin and navigate to the Custom Fields menu item.

Create a new field group called Pet Details.

Click the +Add Field to add a field to our pet details form.

There are a lot of options, but for now, just follow the guide below for each of the fields we want to create.

Pet Type

  • Field Label: Pet Type
  • Field Name: pet_type
  • Field Type: Select
  • Choices: Dog, Cat, Bird, Snake (note the instructions for how to enter these)
  • Required: Yes


  • Field Label: Pet Breed
  • Field Name: pet_breed
  • Field Type: Text
  • Required: Yes


  • Field Label: Pet Age
  • Field Name: pet_age
  • Field Type: Text
  • Required: Yes


  • Field Label: Pet Status
  • Field Name: pet_status
  • Field Type: Radio Button
  • Choices: Available, Pending, Adopted (note the instructions for how to enter these)
  • Required: Yes


  • Field Label: Pet Bio
  • Field Name: pet_bio
  • Field Type: Text Area
  • Required: Yes

Save this field group by clicking the Update button. We’ll come back to this field group later.

Let’s Write Some Code!

The goal here is to build a custom backend and API in WordPress. We’ll be creating a very simple plugin for WordPress to manage our animal shelter API.

Create a directory called shelter and inside that a file called shelter.php.

In order for our plugin to be recognized, we need to add a comment section at the top of the file.

 * Plugin Name: Pet Shelter API
 * Author: Dave Welch

WordPress manages content using post types. In order to keep our data separate from the normal WordPress content, we will create a custom post type.

// Custom Post Type
function petPostType() {
          'labels'      => array(
              'name'          => __('Pets', 'textdomain'),
              'singular_name' => __('Pet', 'textdomain'),
          'public'      => true,
          'has_archive' => true,
          'menu_icon'   => 'dashicons-pets',
add_action('init', 'petPostType');

This code sets our custom post type to “pet” and adds some very basic settings. This post type will serve as the base element for each pet in our database. All of the other information from our ACF form will be connected to this post type as metadata.

The add_action() method hooks into WordPress processes to include our code at the appropriate time. This will automatically show our custom post type on our admin menu when we install the plugin, with a fun little paw icon next to it!

WordPress does the heavy lifting of building the basic UI for us.

Now, let’s create some functions to get the data for our API.

WordPress has its own REST API endpoints to get post and page content. You can add your custom content to that process and access it via their built-in calls. However, I prefer to keep my custom content separate for two reasons.

First, the built-in API calls include a lot of data that I usually don’t want and will have to filter out before sending or ignore once it’s received. I prefer to only send what I want to consume back in the structure.

Second, I like having my own custom routes and being able to decide what they do. The REST calls for filtering can be cumbersome, especially if you are searching by custom metadata. Creating our own calls allows us to handle data and responses however we see fit.

So, let’s keep going.

// Methods
function getPetDetails($post) {
  $fields = get_fields($post->ID);
  return array(
    'id' => $post->ID,
    'name' => $post->post_title,
    'type' => $fields['pet_type'],
    'breed' => $fields['pet_breed'],
    'age' => $fields['pet_age'],
    'status' => $fields['pet_status'],
    'bio' => $fields['pet_bio']

This function will be used by our endpoints to retrieve and format the data we want to send back. get_fields() is an ACF function that gets all the ACF fields for the specific post ID that we are passing in. We then return the data we want to send in an array, which WordPress automatically transforms to JSON.

// API Endpoints
function getAllPets() {
  $posts = get_posts([
    'post_type' => 'pet',
    'numberposts' => -1,
    'post_status' => 'publish'

  $pets = array();

  foreach($posts as $post) {
    $pets[] = getPetDetails($post);

  return $pets;

This is our first endpoint. It’s about as basic as you can get. Here, we are getting all of the pet post types that have been published. The -1 tells WordPress to send everything. Of course, eventually, we would probably want to paginate this, but I’ll let future me worry about that.

We probably need at least a couple more endpoints to get data in different ways. After all, what if we only want to see dogs in our results? Or what if we want to see the details about a specific pet?

function getPetsByType($data) {
  $posts = get_posts([
    'post_type' => 'pet',
    'numberposts' => -1,
    'post_status' => 'publish',
    'meta_key' => 'pet_type',
    'meta_value' => $data['type']

  $pets = array();

  foreach($posts as $post) {
    $pets[] = getPetDetails($post);

  return $pets;

This function is very similar to getAllPets(). However, here, we are adding a couple of parameters to our search. We are searching the metadata for our ACF field pet_type. Then, we are passing in a value from our API route. (We’ll talk more about that soon.)

function getPet($data){
  $post = get_post($data['id']);

  return $post ? getPetDetails($post) : array('error' => 'Invalid Pet ID');

This last endpoint function gets a single pet from the database based on the ID passed via the API route. I’ve included some light error handling. Future me will have to provide something more robust.

So, we have ways of retrieving data and a way to format it however we desire. Now, we just need to be able to accept API calls.

That’s where Routes come in.

// API Routes
function shelterRestRoutes() {

  register_rest_route( 'shelter/v1', '/pets', array(
    'methods' => 'GET',
    'callback' => 'getAllPets'
  ) );

  register_rest_route( 'shelter/v1', '/pets/(?P<type>[a-zA-Z0-9-]+)', array(
    'methods' => 'GET',
    'callback' => 'getPetsByType'
  ) );

  register_rest_route( 'shelter/v1', '/pet/(?P<id>\d+)', array(
    'methods' => 'GET',
    'callback' => 'getPet'
  ) );
add_action( 'rest_api_init', 'shelterRestRoutes' );

There are three separate routes being added to the REST API like this:{namespace}/{route}

We’ve created the following routes based on the namespace shelter/v1:

GET /pets – Gets all pets.

GET /pets/{pet_type} – Gets all pets in a specific type. This accepts alphanumeric characters.

GET /pet/{ID} – Gets a single pet based on ID. This only accepts numbers. You can get this value from the results of the other two routes.

Now, when we install this plugin, these routes will become available for us to access via the browser, postman, or other API testing tool.

Let’s zip up this directory, upload it with the WordPress plugin uploader, and activate it.

Finish With ACF

Now that we’ve installed our new plugin, you should notice a new menu item on the admin menu that says, “Pets,” with a nice animal paw icon. This is where we will be able to add pets to the system. However, we have a couple more things to do in ACF before concluding.

Go back to the Custom Fields menu item and open our Pet Details form group again.

Once it loads, scroll down to the Location section. You will see a setting with the words, “Show this field group if.” Use the select boxes to create this rule: Post Type - is equal to - Pet.

Under the Settings section, make sure this field group is marked as Active. Then scroll down to the ‘Hide on screen’ setting. Click the ‘Toggle All’ option and then update the field group.

We just assigned the field group to show up on our new custom post type. This will not only show the form when we add or edit a pet, but it will also handle saving the metadata info from ACF fields when we save the pet.

Now, go add a few pets to your shelter via the Pets menu item. Once you’ve done that, it’s time to test it out.

Try each of the endpoints:{ID}

You should see data flowing right away that looks something like this:

[{"id":15,"name":"Roxy","type":"dog","breed":"Besengi \/ Beagle","age":"9","status":"available","bio":"Roxy is an excellent dog. You will really love her and she will really love you!"}]

Wrapping Up

And we’re done! Hopefully, you’ve successfully built a simple custom backend with an API in WordPress that can be consumed by your fancy javascript framework of choice; a solution that appeases many opinions. Now, you can translate this to your own work.

Of course, there are many, many things we could do to make this a more robust website/application, but we’ll leave that for future you and future me to discover another time!

4.3 3 votes
Article Rating
Notify of
Inline Feedbacks
View all comments