Web API of the simple kind

A method for creating simple web APIs

I didn't come up with this. I inherited the job of bringinng a live prototype into something of a stable and usable web API for our customers to book couriers. However it's quite a cool little technique.

As I'm writing this the most common web APIs are RESTful, SOAP, or even XMLRPC. However you can do quite a lot with plain old HTML POST. You can do even more with one of the fields being XML or JSON and it works well with AJAX techniques as well.

Now how many more acronyms could I have put in that last paragraph. :)

An example

The best way to show how this can work - I think - is with a simple example. I'm going to make this so simple it won't be bordering on silly, it will be completely across the border on holiday, mispronouncing the local language badly while complaining the food is better at home.

It's going to take a first name, a surname, put them together, and return the result. Below is an html page with a form to test out the PHP code. It uses jQuery to do an AJAX call of the API.

Simple HTML form using jQuery for AJAX

<!doctype html>
<html>
    <head>
        <script src='http://code.jquery.com/jquery-1.11.1.min.js'></script>
        <script>
            $(document).ready( function() {
                $('#callapi').click( callapi );
            });
            function callapi() {
                $.ajax({
                    type: 'POST',
                    url: 'mysimpleapi.php',
                    dataType: 'text',
                    data: { xml: $('#xml').val() }
                })
                .done( function( ret ) {
                    $('#retxml').val( ret );
                });
            }
        </script>
    </head>
    <body>
        <form action='mysimpleapi.php' method=POST>
            <label for=xml>XML</label><br/>
            <textarea cols=80 rows=10 id=xml name=xml >
<?xml version='1.0' encoding='UTF-8'?>
<request function='combineName'>
 <firstname>Nigel</firstname>
 <surname>Atkinson</surname>
</request>
            </textarea><br/>
            <input id=callapi type=button value='Call API'/>
        </form>
        <p>Returned XML</p>
        <textarea id=retxml cols=80 rows=20></textarea>
    </body>
</html>

Now for the server side. Nearly all of the below code is what you could call 'boiler-plate', but that is what we are interested in. I've included some code that gives you some more helpful errors, should the XML you send be not correct.

You can organise the XML request and response how you please. Here I have a root node called 'request', which has an attribute saying which API function you want. The API returns XML with a root node of 'response'. If you were using JSON instead, things would be very similar, except likely you will have nested objects or arrays for selecting the function and giving parameters.

Notice the switch on line 30. You could have something like $function( $xml, $response ); instead of having a potentially large switch/case statement. However you still need to check that the function name is valid, as well as being a function you want to be 'public' rather than one that just happens to be in your code base, or even part of php. The switch also enables you to have different names for functions than what they are called in the API.

mysimpleapi.php

<?php
// Parse the errors ourselves, so we can be more helpful.
libxml_use_internal_errors(true);
define( 'ERROR_PARSE', 1 );
define( 'ERROR_NO_ATTRIBUTE', 2 );
define( 'ERROR_UNKNOWN_FUNC', 3 );

$xmlstr = $_POST['xml'];

$xml = simplexml_load_string( $xmlstr );

if( ! $xml ) {
    foreach( libxml_get_errors() as $error ) {
        $errors .= format_xml_error( $error, $xmlstr );
    }
    libxml_clear_errors();
    returnErrorXML( "\nThere was a problem with the request:\n" . $errors, ERROR_PARSE );
    file_put_contents( '/tmp/mysimpleapi.log', print_r( $_POST, true ) . $errors );
    die();
}

if( ! isset( $xml->attributes()['function'] ) ) {
    returnErrorXML( "No attribute set for 'function'.", ERROR_NO_ATTRIBUTE );
    die();
}

$function = $xml->attributes()['function'];
$response = simplexml_load_string( "<?xml version='1.0' encoding='UTF-8'?><response/>" );

switch( $function ) {
case 'combineName':
    combineName( $xml, $response );
    break;
default:
    returnErrorXML( "Unknown function: " . $function, ERROR_UNKNOWN_FUNC );
    die();
}

returnXML( $response );

exit();

function returnErrorXML( $errorStr, $code )
{
    $response = simplexml_load_string( 
        "<?xml version='1.0' encoding='UTF-8'?><response><error/></response>" );
    $response->error->text = $errorStr;
    $response->error->code = $code;

    returnXML( $response );
}

function returnXML( &$xmlobj )
{
    header("Content-type: text/xml; charset=utf-8");
    echo $xmlobj->asXML();
}

// This function is 99% directly taken from:
// http://php.net/manual/en/function.libxml-get-errors.php
// Why re-invent the wheel?!
function format_xml_error($error, $xml)
{
    $xml = explode( "\n", $xml );
    $return  = $xml[$error->line - 1] . "\n";
    $return .= str_repeat('-', $error->column) . "^\n";

    switch ($error->level) {
    case LIBXML_ERR_WARNING:
        $return .= "Warning $error->code: ";
        break;
    case LIBXML_ERR_ERROR:
        $return .= "Error $error->code: ";
        break;
    case LIBXML_ERR_FATAL:
        $return .= "Fatal Error $error->code: ";
        break;
    }

    $return .= trim($error->message) .
        "\n  Line: $error->line" .
        "\n  Column: $error->column";

    if ($error->file) {
        $return .= "\n  File: $error->file";
    }

    return "$return\n\n--------------------------------------------\n\n";
}

// ###############################################################################
// Here is a actual code that does the work.
function combineName( &$xml, &$response )
{
    $response->name = $xml->firstname . ' ' . $xml->surname;
}

So there you have it. Not that hard really.

You will want to add some security should your API deal with anything remotely sensitive, or you need to know who is who. You could add security with required fields in the XML/JSON, however it is just as easy to have more POST variables such as username and password, or a hashed 'api key'.

JSON version

Using JSON rather than XML is not that different, however making this I had to play around with the jQuery AJAX call a bit to make UTF-8 work correctly. It works fine if you tell jQuery it is JSON, rather than text like I did with the XML example.

<!doctype html>
<html>
    <head>
        <meta charset='utf-8'>
        <script src='http://code.jquery.com/jquery-1.11.1.min.js'></script>
        <script>
            $(document).ready( function() {
                $('#callapi').click( callapi );
            });
            function callapi() {
                $.ajax({
                    type: 'POST',
                    url: 'mysimpleapi_json.php',
                    dataType: 'json',
                    data: { json: $('#json').val() }
                })
                .done( function( ret ) {
                    $('#retjson').val( JSON.stringify(ret)
                      + "\n\n-----------8<---------------8<---------------\n\n"
                      + dump( ret ) );
                });
            }
            function dump( obj ) {
                var out = '';
                for( var i in obj ) {
                    out += i + ": " + obj[i] + "\n";
                }
                return out;
            }
        </script>
    </head>
    <body>
        <form>
            <label for=json>JSON</label><br/>
            <textarea cols=80 rows=10 id=json name=json >
{
 "function": "combineName",
 "parameters": {
  "firstname": "Nigel",
  "surname": "Atkinson"
 }
}
            </textarea><br/>
            <input id=callapi type=button value='Call API'/>
        </form>
        <p>Returned data</p>
        <textarea id=retjson cols=80 rows=20></textarea>
    </body>
</html>

JSON server side

<?php

$request = json_decode( $_POST['json'], true, 32 );

if( $request ==  null ) {
    returnJSONError();
}

$response = array();
$response['status'] = 'ok';

switch( $request['function'] ) {
case 'combineName':
    combineName( $response, $request );
    returnJSON( $response );
    break;
default:
    returnError( 'Unknown function' );
    break;
}

exit(); // Shouldn't get here, as there is an exit in 'returnJSON'

function returnJSONError()
{
    $errorMessage = 'There was an error decoding the request ';

    switch (json_last_error()) {
    case JSON_ERROR_DEPTH:
        $errorMessage .= ' - Maximum stack depth exceeded';
        break;
    case JSON_ERROR_STATE_MISMATCH:
        $errorMessage .= ' - Underflow or the modes mismatch';
        break;
    case JSON_ERROR_CTRL_CHAR:
        $errorMessage .= ' - Unexpected control character found';
        break;
    case JSON_ERROR_SYNTAX:
        $errorMessage .= ' - Syntax error, malformed JSON';
        break;
    case JSON_ERROR_UTF8:
        $errorMessage .= ' - Malformed UTF-8 characters, possibly incorrectly encoded';
        break;
    default:
        $errorMessage .= ' - Unknown error';
        break;
    }

    returnError( $errorMessage );
}

function returnError( $error )
{
    $return = array();
    $return['error'] = $error;
    $return['status'] = 'failed';

    returnJSON( $return );
}

function returnJSON( array & $response )
{
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode( $response );
    exit();
}

// #####################################################################

function combineName( array & $response, array & $request )
{
    $response['fullname'] =
        $request['parameters']['firstname'] . ' ' .
        $request['parameters']['surname'];
}

Calling from a server

Below is an example of the sort of PHP code I use to call these type of APIs from another server. In my usage of these APIs, this is just as common as calling from the client. Depending on how you have things set up, you may or may not need the SSL options.

<?php
// Note: tlog is just a function that just logs errors. :)

function call_api( $api_url, &$xml )
{
    $fields = array(
        'xml' => $xml->asXML(),
    );

    $ch = curl_init( $api_url );
    //curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false);
    //curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0 );
    curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
    curl_setopt( $ch, CURLOPT_HEADER, false );
    curl_setopt( $ch, CURLOPT_POST, true );
    curl_setopt( $ch, CURLOPT_POSTFIELDS, $fields );

    $ret = curl_exec( $ch );

    if( $ret == false )
        tlog( curl_error( $ch ) );

    curl_close( $ch );

    try
    {
        $xml_resp = new SimpleXMLElement( $ret );
    }
    catch( Exception $e )
    {
        tlog( "Error with response from API: " . $e->getMessage() );
        tlog( $ret . "\n" );
        return -1;
    }

    return $xml_resp;
}
Back to Top