Sunday April 17, 2011

✦ On Abstracting Server APIs

I got a question from a friend who is getting into iOS development and wanted some good practices to talk to a simple JSON API. As I was writing back, I realized my response could serve as a great post.

If you want a quick and simple library for JSON APIs, you’d do fine with Seriously. Seriously! I’ve used it in a couple of projects already. It’s a nice wrapper around NSURLConnection using the block syntax and NSOperationQueues. It will automatically convert a response body with mime type of application/json into an NSDictionary. It’s small and easy enough to follow that you can maintain it yourself.

Here’s how you can use it. Pretend that we are building some sort of messaging system. There’s an object, CMMessage, that we need to populate from the data on the server.

NSString *url = nil;
NSDictionary *headers = nil, *options = nil;

// messageId is which message we're trying to fetch
url = [NSString stringWithFormat:@"http://some-server.foo/messages/%@", messageId];

// Authenticate with a user token in a customer HTTP header
headers = [NSDictionary dictionaryWithObject:userToken forKey:@"X-User-Token"];
options = [NSDictionary dictionaryWithObject:headers forKey:kSeriouslyHeaders];

// Make the GET request with the response handler
[Seriously get:url options:options handler:^(id body, NSHTTPURLResponse *response, NSError *error) {
    NSInteger statusCode = [response statusCode];

    if (statusCode == 200) {
        // On success, "body" is a dictionary from the JSON
        // Pretend CMMessage has a class method that knows how to
        // instantiate and populate from a dictionary.
        CMMessage *message = [CMMessage messageFromDictionary:body];

        NSLog(@"Message Subject: %@", message.subject);
    }

    // If we don't succeed, find out what happened and tell the
    // error handler
    else if (statusCode == 401) {
        // Don't have access ...
    }
    else if (statusCode == 404) {
        // Didn't find it
    }
    else {
        // We have bigger problems
    }

}];

In this example, I’m authenticating as the user with a custom HTTP header. Once the URL and header are set up, we invoke a GET request using Seriously's class method. It takes a block that will be invoked once the request is complete. The body parameter of that block is the response content.

We fetch the status code from the response parameter and decide if it’s an error or not. If successful (and the response mime type is JSON), then body is now just an NSDictionary. In this example, I create a new autoreleased CMMessage object using a class method that takes that dictionary to fill it in. We now have our message we can use in the rest of the app.

It’s pretty straightforward to make PUT, POST, or other verb calls to the server. Check the Seriously.h header for more info.

But Wait, There’s More

But, before you start sprinkling code like this throughout your app, I recommend considering another layer of abstraction. We don’t want to set up the authentication headers in place every time. We want our error handling conditions to be self evident and meaningful to our application’s domain.

Let’s wrap the concept of talking to the server in it’s own object. And let’s make a very specific method to ask for a CMMessage. I know some of this may seem pedantic, but bear with me.

#import "Seriously.h"

// We'll first define some simplified error codes
// that we can hand back to our error handler
typedef enum {
    CMServerErrorNotFound,
    CMServerErrorNotAuthorized,
    CMServerErrorBiggerProblem
} CMServerErrorCode;

// Next, we'll define an error handling block that takes an error code
typedef void(^CMServerErrorHandler)(CMServerErrorCode errorCode);

// A callback block for successfully retrieving a message
typedef void(^CMServerGetMessageHandler)(CMMessage *message);

@interface CMServer : NSObject {}

// And here's the declaration for fetching a message
- (void)getMessageWithID:(NSString *)messageId
               userToken:(NSString *)userToken
                callback:(CMServerGetMessageHandler)callback
            errorHandler:(CMServerErrorHandler)errorHandler;

@end

I’ll talk about those custom error codes in a moment.

We declare two block types that we’ll use in the method definition. The first is the error handler. The second is specific for this “get message” method. As I have more things to ask of the server, I add more methods and, if need be, I add more response block types. In this case, we have a block that expects to get passed an instantiated CMMessage object.

Let’s jump to the implementation.

- (void)getMessageWithID:(NSString *)messageId
               userToken:(NSString *)userToken
                callback:(CMServerGetMessageHandler)callback
            errorHandler:(CMServerErrorHandler)errorHandler
{
    NSString *url = nil;
    NSDictionary *headers = nil, *options = nil;

    url = [NSString stringWithFormat:@"http://some-server.foo/messages/%@", messageId];

    headers = [NSDictionary dictionaryWithObject:userToken forKey:@"X-User-Token"];
    options = [NSDictionary dictionaryWithObject:headers forKey:kSeriouslyHeaders];

    [Seriously get:url options:options handler:^(id body, NSHTTPURLResponse *response, NSError *error) {
        NSInteger statusCode = [response statusCode];

        if (statusCode == 200) {
            CMMessage *fetchedMessage = [CMMessage messageFromDictionary:body];

            // Hand back the message to the caller
            callback(fetchedMessage);
        }

        // If we don't succeed, find out what happened and tell the
        // error handler
        else if (statusCode == 401) {
            errorHandler(CMServerErrorNotAuthorized);
        }
        else if (statusCode == 404) {
            errorHandler(CMServerErrorNotFound);
        }
        else {
            errorHandler(CMServerErrorBiggerProblem);
        }

    }];
}

You’ll recognize most of the message body from our raw example above. But note how the blocks are used. On success, we’ll instantiate the CMMessage object from the NSDictionary and then hand it in to the CMServerGetMessageHandler callback. That’s it. This object’s work is done. What’s going to happen to that CMMessage? That is up to the caller now.

Also, notice how we map other status codes to our custom errors as we call the CMServerErrorHandler. In a more complicated application, the server may send back a 400 error status and then more details in the body. Maybe it would be meaningful to have a CMServerErrorMessageMoved. It’s this object’s job to map the conditions to the error code that makes sense to the rest of the application.

Okay, so how do we use this?

CMServer *server = [[CMServer alloc] init];
[server getMessageWithID:messageId userToken:userToken callback:^(CMMessage * message) {

    // Aha! We have a fully populated message from the server!

    NSLog(@"Message Subject: %@", message.subject);

} errorHandler:^(CMServerErrorCode errorCode) {

    // Oops. Something went wrong.
    switch (errorCode) {
        case CMServerErrorNotFound:
            // ...
            break;
        case CMServerErrorNotAuthorized:
            // ...
            break;
        case CMServerErrorBiggerProblem:
            // raiseHell();
            break;
    }

}];

Ah, much cleaner. We don’t have to handle authentication headers or url generation inline anymore. Anywhere in the app that we have to get a message from the server, we can do it with a minimum of fuss. By isolating how errors are translated from the server response, your code will be cleaner and clearer to other developers. And let’s not forget that when you come back to this in 6 months you will be that “other developer”.

So, use Seriously as a library to talk JSON to your server. But consider using this pattern along with it. Isolate the code that talks to the server. Translate the responses into the domain model of your application.