Writing a RESTful API

HTTP Methods

When writing your APIs, remember that they will be responding to HTTP requests from the front end.

The API must respond to each expected HTTP Method:

  • GET — Read Requests (SELECT) from the database.
  • POST — Create a New Object and INSERT into the database.
  • PUT — Modify an Existing Object and UPDATE the database.
  • DELETEDELETE an Object from the database.

In some specfic cases, an API may not be required to respond to all four methods. For example, a Log-In or Sign-Up API tyically only needs to respond to the POST method. You should write the API for all required methods, no more and no less.


RESTful Services

REST, a mnemonic for Representational State Transfer, is a software architectural pattern that lays out how to interchange data amongst different sites. REST allows sites with different backends, different languages, different use cases, etc. to easily exchange information without the complication of writing site-to-site specific integration code. REST rides on top of standard HTTP verbs to perform a standard set of operations. For instance, suppose we want to share the tweet entity from the object oriented example. To make the entity REST compatible, we would write code that would map the different HTTP verbs to different object oriented methods.

Table 1: Mapping HTTP Verbs to Object Oriented Methods
HTTP VerbObject Oriented MethodComment
GET /tweet/Tweet::getAllTweets($pdo);Get All Available Data
GET /tweet/4de172cd-1b61-4cce-9efb-f0cf1f153e5eTweet::getTweetByTweetId($pdo, "4de172cd-1b61-4cce-9efb-f0cf1f153e5e");Get a Tweet by Primary Key
DELETE /tweet/4de172cd-1b61-4cce-9efb-f0cf1f153e5e$tweet = Tweet::getTweetByTweetId($pdo, "4de172cd-1b61-4cce-9efb-f0cf1f153e5e");
$tweet->delete($pdo);
Delete a Tweet by Primary Key
PUT /tweet/4de172cd-1b61-4cce-9efb-f0cf1f153e5e$tweet = Tweet::getTweetByTweetId($pdo, "4de172cd-1b61-4cce-9efb-f0cf1f153e5e");
$tweet->update($pdo);
Update a Tweet by Primary Key
POST /tweet/$tweet->insert($pdo);Create a Brand New Tweet

Table 1 maps the translation from HTTP verbs to object oriented methods. By following this map, one can turn any class into a REST compliant class.


Core Structure of the REST API

The structure of the core of the API will look something like this:

<?php

// we determine if we have a GET request. If so, we then process the request.
if ($method === "GET") {


// If it is not a GET request, we then proceed here to determine if we have a PUT or POST request.
} else if($method === "PUT" || $method === "POST") {

	//do setup that is needed for both PUT and POST requests

		//perform the actual put or post
	if($method === "PUT") {
		// determines if we have a PUT request. If so we process the request.
		// process PUT requests here


		} else if ($method === "POST") {

		// process the POST request  here

		}


		// if the above requests are neither a PUT or POST delete below
} else if($method === "DELETE") {

	// process DELETE requests here

}


It is simply a stack of if and else if statments. We use these statments to determine the type of HTTP method that needs to be processed, and then process that request in the appropriate if block.

Getting Started

For this API example, we will be referencing the Tweet Class.

After looking at the class, we see that this is what we want our API to do:

  • GET all Tweets
  • GET a specific Tweet by Primary Key
  • POST - Create a brand new Tweet
  • PUT - Update Tweet by Primary Key
  • DELETE - Delete a Tweet by Primary Key
<?php

require_once dirname(__DIR__, 3) . "/vendor/autoload.php";
require_once dirname(__DIR__, 3) . "/php/classes/autoload.php";
require_once dirname(__DIR__, 3) . "/php/lib/xsrf.php";
require_once dirname(__DIR__, 3) . "/php/lib/uuid.php";
require_once("/etc/apache2/capstone-mysql/encrypted-config.php");

use Edu\Cnm\DataDesign\{
	Tweet,
	// we only use the profile class for testing purposes
	Profile
};


/**
 * api for the Tweet class
 *
 * @author {} <valebmeza@gmail.com>
 * @coauthor Derek Mauldin <derek.e.mauldin@gmail.com>
 **/

//verify the session, start if not active
if(session_status() !== PHP_SESSION_ACTIVE) {
	session_start();
}

//prepare an empty reply
$reply = new stdClass();
$reply->status = 200;
$reply->data = null;

try {
	//grab the mySQL connection
	$pdo = connectToEncryptedMySQL("/etc/apache2/capstone-mysql/ddctwitter.ini");

	//determine which HTTP method was used
	$method = $_SERVER["HTTP_X_HTTP_METHOD"] ?? $_SERVER["REQUEST_METHOD"];

	//sanitize input
	$id = filter_input(INPUT_GET, "id", FILTER_SANITIZE_STRING,FILTER_FLAG_NO_ENCODE_QUOTES);
	$tweetProfileId = filter_input(INPUT_GET, "tweetProfileId", FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
	$tweetContent = filter_input(INPUT_GET, "tweetContent", FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);

	//make sure the id is valid for methods that require it
	if(($method === "DELETE" || $method === "PUT") && empty($id) === true) {
		throw(new InvalidArgumentException("id cannot be empty", 405));
	}


	// handle GET request - if id is present, that tweet is returned, otherwise all tweets are returned
	if($method === "GET") {
		//set XSRF cookie
		setXsrfCookie();

		//get a specific tweet or all tweets and update reply
		if(empty($id) === false) {
			$reply->data = Tweet::getTweetByTweetId($pdo, $id);
		} else if(empty($tweetProfileId) === false) {
			$reply->data = Tweet::getTweetByTweetProfileId($pdo, $tweetProfileId)->toArray();
		} else if(empty($tweetContent) === false) {
			$reply->data = Tweet::getTweetByTweetContent($pdo, $tweetContent)->toArray();
		} else {
			$reply->data = Tweet::getAllTweets($pdo)->toArray();
		}
	} else if($method === "PUT" || $method === "POST") {

		//enforce that the user has an XSRF token
		verifyXsrf();

		$requestContent = file_get_contents("php://input");
		// Retrieves the JSON package that the front end sent, and stores it in $requestContent. Here we are using file_get_contents("php://input") to get the request from the front end. file_get_contents() is a PHP function that reads a file into a string. The argument for the function, here, is "php://input". This is a read only stream that allows raw data to be read from the front end request which is, in this case, a JSON package.
		$requestObject = json_decode($requestContent);
		// This Line Then decodes the JSON package and stores that result in $requestObject

		//make sure tweet content is available (required field)
		if(empty($requestObject->tweetContent) === true) {
			throw(new \InvalidArgumentException ("No content for Tweet.", 405));
		}

		// make sure tweet date is accurate (optional field)
		if(empty($requestObject->tweetDate) === true) {
			$requestObject->tweetDate = null;
		} else {
			// if the date exists, Angular's milliseconds since the beginning of time MUST be converted
			$tweetDate = DateTime::createFromFormat("U.u", $requestObject->tweetDate / 1000);
			if($tweetDate === false) {
				throw(new RuntimeException("invalid tweet date", 400));
			}
			$requestObject->tweetDate = $tweetDate;
		}

		//perform the actual put or post
		if($method === "PUT") {

			// retrieve the tweet to update
			$tweet = Tweet::getTweetByTweetId($pdo, $id);
			if($tweet === null) {
				throw(new RuntimeException("Tweet does not exist", 404));
			}

			//enforce the user is signed in and only trying to edit their own tweet
			if(empty($_SESSION["profile"]) === true || $_SESSION["profile"]->getProfileId()->toString() !== $tweet->getTweetProfileId()->toString()) {
				throw(new \InvalidArgumentException("You are not allowed to edit this tweet", 403));
			}

			// update all attributes
			$tweet->setTweetDate($requestObject->tweetDate);
			$tweet->setTweetContent($requestObject->tweetContent);
			$tweet->update($pdo);

			// update reply
			$reply->message = "Tweet updated OK";

		} else if($method === "POST") {

			// enforce the user is signed in
			if(empty($_SESSION["profile"]) === true) {
				throw(new \InvalidArgumentException("you must be logged in to post tweets", 403));
			}

			// create new tweet and insert into the database
			$tweet = new Tweet(generateUuidV4(), $_SESSION["profile"]->getProfileId(), $requestObject->tweetContent, null);
			$tweet->insert($pdo);

			// update reply
			$reply->message = "Tweet created OK";
		}

	} else if($method === "DELETE") {

		//enforce that the end user has a XSRF token.
		verifyXsrf();

		// retrieve the Tweet to be deleted
		$tweet = Tweet::getTweetByTweetId($pdo, $id);
		if($tweet === null) {
			throw(new RuntimeException("Tweet does not exist", 404));
		}

		//enforce the user is signed in and only trying to edit their own tweet
		if(empty($_SESSION["profile"]) === true || $_SESSION["profile"]->getProfileId() !== $tweet->getTweetProfileId()) {
			throw(new \InvalidArgumentException("You are not allowed to delete this tweet", 403));
		}

		// delete tweet
		$tweet->delete($pdo);
		// update reply
		$reply->message = "Tweet deleted OK";
	} else {
		throw (new InvalidArgumentException("Invalid HTTP method request"));
	}
// update the $reply->status $reply->message
} catch(\Exception | \TypeError $exception) {
	$reply->status = $exception->getCode();
	$reply->message = $exception->getMessage();
}

// encode and return reply to front end caller
header("Content-type: application/json");
echo json_encode($reply);

Setup

<?php
require_once dirname(__DIR__, 3) . "/vendor/autoload.php";
require_once dirname(__DIR__, 3) . "/php/classes/autoload.php";
require_once dirname(__DIR__, 3) . "/php/lib/xsrf.php";
require_once dirname(__DIR__, 3) . "/php/lib/uuid.php";
require_once("/etc/apache2/capstone-mysql/encrypted-config.php");

use Edu\Cnm\DataDesign\{
	Tweet,
	// we only use the profile class for testing purposes
	Profile
};


/**
 * api for the Tweet class
 *
 * @author {} <valebmeza@gmail.com>
 * @coauthor Derek Mauldin <derek.e.mauldin@gmail.com>
 **/

//verify the session, start if not active
if(session_status() !== PHP_SESSION_ACTIVE) {
	session_start();
}

//prepare an empty reply
$reply = new stdClass();
$reply->status = 200;
$reply->data = null;

try {
	//grab the mySQL connection
	$pdo = connectToEncryptedMySQL("/etc/apache2/capstone-mysql/ddctwitter.ini");

	//determine which HTTP method was used
	$method = $_SERVER["HTTP_X_HTTP_METHOD"] ?? $_SERVER["REQUEST_METHOD"];

	//sanitize input
	$id = filter_input(INPUT_GET, "id", FILTER_SANITIZE_STRING,FILTER_FLAG_NO_ENCODE_QUOTES);
	$tweetProfileId = filter_input(INPUT_GET, "tweetProfileId", FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
	$tweetContent = filter_input(INPUT_GET, "tweetContent", FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);

	//make sure the id is valid for methods that require it
	if(($method === "DELETE" || $method === "PUT") && (empty($id) === true)) {
		throw(new InvalidArgumentException("id cannot be empty or negative", 405));
	}




GET

<?php
if($method === "GET") {
	//set XSRF cookie
	setXsrfCookie();

	//get a specific tweet based on arguments provided or all the tweets and update reply
	if(empty($id) === false) {
		$reply->data = Tweet::getTweetByTweetId($pdo, $id);
	} else if(empty($tweetProfileId) === false) {
		$reply->data = Tweet::getTweetByTweetProfileId($pdo, $tweetProfileId)->toArray();
	} else if(empty($tweetContent) === false) {
		$reply->data = Tweet::getTweetByTweetContent($pdo, $tweetContent)->toArray();
	} else {
		$reply->data = Tweet::getAllTweets($pdo)->toArray();
	}
}


PUT and POST

<?php
else if($method === "PUT" || $method === "POST") {

	// enforce the user has a XSRF token
	verifyXsrf();

	//  Retrieves the JSON package that the front end sent, and stores it in $requestContent. Here we are using file_get_contents("php://input") to get the request from the front end. file_get_contents() is a PHP function that reads a file into a string. The argument for the function, here, is "php://input". This is a read only stream that allows raw data to be read from the front end request which is, in this case, a JSON package.
	$requestContent = file_get_contents("php://input");

	// This Line Then decodes the JSON package and stores that result in $requestObject
	$requestObject = json_decode($requestContent);

	//make sure tweet content is available (required field)
	if(empty($requestObject->tweetContent) === true) {
		throw(new \InvalidArgumentException ("No content for Tweet.", 405));
	}

	// make sure tweet date is accurate (optional field)
	if(empty($requestObject->tweetDate) === true) {
		$requestObject->tweetDate = null;
	} else {
		// if the date exists, Angular's milliseconds since the beginning of time MUST be converted
		$tweetDate = DateTime::createFromFormat("U.u", $requestObject->tweetDate / 1000);
		if($tweetDate === false) {
			throw(new RuntimeException("invalid tweet date", 400));
		}
		$requestObject->tweetDate = $tweetDate;
	}

	//  make sure profileId is available
	if(empty($requestObject->tweetProfileId) === true) {
		throw(new \InvalidArgumentException ("No Profile ID.", 405));
	}

	//perform the actual put or post
	if($method === "PUT") {

		// retrieve the tweet to update
		$tweet = Tweet::getTweetByTweetId($pdo, $id);
		if($tweet === null) {
			throw(new RuntimeException("Tweet does not exist", 404));
		}

		//enforce the user is signed in and only trying to edit their own tweet
		if(empty($_SESSION["profile"]) === true || $_SESSION["profile"]->getProfileId()->toString() !== $tweet->getTweetProfileId()->toString()) {
			throw(new \InvalidArgumentException("You are not allowed to edit this tweet", 403));
		}

		// update all attributes
		$tweet->setTweetDate($requestObject->tweetDate);
		$tweet->setTweetContent($requestObject->tweetContent);
		$tweet->update($pdo);

		// update reply
		$reply->message = "Tweet updated OK";

	} else if($method === "POST") {

		// enforce the user is signed in
		if(empty($_SESSION["profile"]) === true) {
			throw(new \InvalidArgumentException("you must be logged in to post tweets", 403));
		}

		// create new tweet and insert into the database
		$tweet = new Tweet(generateUuidV4(), $_SESSION["profile"]->getProfileId, $requestObject->tweetContent, null);
		$tweet->insert($pdo);

		// update reply
		$reply->message = "Tweet created OK";
	}

}

The first part of the PUT and POST section sets up items that are needed by both types of requests. We are handling PUT and POST together in an outer if block and then in an inner if else block we process the request.



DELETE

<?php


else if($method === "DELETE") {

	//enforce that the end user has a XSRF token.
	verifyXsrf();

	// retrieve the Tweet to be deleted
	$tweet = Tweet::getTweetByTweetId($pdo, $id);
	if($tweet === null) {
		throw(new RuntimeException("Tweet does not exist", 404));
	}

	//enforce the user is signed in and only trying to edit their own tweet
	if(empty($_SESSION["profile"]) === true || $_SESSION["profile"]->getProfileId() !== $tweet->getTweetProfileId()) {
		throw(new \InvalidArgumentException("You are not allowed to delete this tweet", 403));
	}

	// delete tweet
	$tweet->delete($pdo);
	// update reply
	$reply->message = "Tweet deleted OK";
}


Finishing Up

<?php

// update the $reply->status $reply->message
} catch(\Exception | \TypeError $exception) {
	$reply->status = $exception->getCode();
	$reply->message = $exception->getMessage();
}

// encode and return reply to front end caller
header("Content-type: application/json");
echo json_encode($reply);

// finally - JSON encodes the $reply object and sends it back to the front end.
Warning! The Catch for TypeError is for PHP 7 only. Using other version of PHP you will want to omit lines 125 - 128.

One Last Thing

In the same directory as your API, you will want to include a .htaccess file. This is a seperate file from the API. This file will do some configuration on the Apache server to support your API.

<IfModule mod_rewrite.c>
	RewriteEngine on
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule ^/?([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})?$ ?id=$1&%{QUERY_STRING}
</IfModule>

The second to last line in this file is the Line that makes sure that the Primary Key sent in the URL will be processed properly as long as you keep the $id variable in the API.