Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a flexible API with grails

Tags:

grails

api

So a little background. I'm creating a website with quite a comprehensive api. The api should be able to handle changes, so therefore i have versioned the api, with api url equivalent to something like /api/0.2/$apiKey/$controller/$action/$id.

I want to be able to reuse my controllers for the api as well as the standard html view. The solution was at first to use withFormat block in all my actions (through a private function shared between used in my action blocks).

I don't like duplicate code, and therefore i want to centralize withFormat functionality. so instead of having a bunch of controllers and actions having their own withFormat block I would like it to be either a service (however, we don't have access to render() on services, do we?), or have a filter which can render the output according to grails content negotiation.

My current solution has this filter defined:

            after = { model ->
            def controller = grailsApplication.controllerClasses.find { controller ->
                controller.logicalPropertyName == controllerName
            }
            def action = applicationContext.getBean(controller.fullName).class.declaredFields.find{ field -> field.name == actionName }

            if(model && (isControllerApiRenderable(controller) || isActionApiRenderable(action))){
                switch(request.format){
                    case 'json':
                        render text:model as JSON, contentType: "application/json"
                        return false
                    case 'xml':
                        render text:model as XML, contentType: "application/xml"
                        return false
                    default:
                        render status: 406
                        return false
                }
            }
            return true
        }

As an example, all i have to do in the controller to render xml or json is:

@ApiRenderable
def list = {
  def collectionOfSomething = SomeDomain.findAllBySomething('someCriteria')
  return [someCollection:collectionOfSomething]
}

now if i access the the url that triggers this action list, (/api/0.2/apikey/controller/list.json or /api/0.2/apikey/controller/list?format=json or with headers: content-type: application/json) then the response would be encoded as follows:

{

      someCollection: [
          {
              someData: 'someData'
          },
          {
              someData: 'someData2'
          }  
      ]

}

This is all very good if I always want to return a hashmap (which currently is a requirement from the controllers), but in this example all I wanted to return was the actual list! not a list wrapped within a hashmap....

Does anyone have any pointers on how to create a good api functionality that is robust as well as flexible and that follows the DRY principle, that can handle versioning (/api/0.1/, /api/0.2/), and that can handle different marshalling approaches depending on the context in which it is returned? Any tips is appreciated!

like image 672
netbrain Avatar asked Apr 28 '11 19:04

netbrain


People also ask

What makes RESTful API?

A RESTful API is an architectural style for an application program interface (API) that uses HTTP requests to access and use data. That data can be used to GET, PUT, POST and DELETE data types, which refers to the reading, updating, creating and deleting of operations concerning resources.

What is a RESTful API example?

An application implementing a RESTful API will define one or more URL endpoints with a domain, port, path, and/or query string — for example, https://mydomain/user/123?format=json . The HTTP method.


2 Answers

Ok, so here is what i have done so far, which i believe give me quite a bit of flexibility. This is probably a lot to read, but any suggestions on improvements or changes is greatly appreciated!

Custom filter

class ApiFilters {

    def authenticateService

    def filters = {
        authenticateApiUsage(uri:"/api/**") {
            before = {
                if(authenticateService.isLoggedIn() || false){
                    //todo authenticate apiKey and apiSession
                    return true
                }else{
                    return false
                }
            }
            after = {
            }
            afterView = {
            }
        }
        renderProperContent(uri:"/api/**"){
            before = {
                //may be cpu heavy operation using reflection, initial tests show 100ms was used on first request, 10ms on subsequent.
                def controller = grailsApplication.controllerClasses.find { controller ->
                    controller.logicalPropertyName == controllerName
                }
                def action = applicationContext.getBean(controller.fullName).class.declaredFields.find{ field -> field.name == actionName }

                if(isControllerApiRenderable(controller) || isActionApiRenderable(action)){
                    if(isActionApiCorrectVersion(action,params.version)){
                        return true
                    }else{
                        render status: 415, text: "unsupported version"
                        return false
                    }
                }
            }
            after = { model ->
               if (model){
                   def keys = model.keySet()
                   if(keys.size() == 1){
                       model = model.get(keys.toArray()[0])
                   }
                   switch(request.format){
                       case 'json':
                            render text:model as JSON, contentType: "application/json"
                            break
                       case 'xml':
                            render text:model as XML, contentType: "application/xml"
                            break
                       default:
                            render status: 406
                            break
                   }
                   return false

                }
                return true
            }
        }
    }

    private boolean isControllerApiRenderable(def controller) {
        return ApplicationHolder.application.mainContext.getBean(controller.fullName).class.isAnnotationPresent(ApiEnabled)
    }

    private boolean isActionApiRenderable(def action) {
        return action.isAnnotationPresent(ApiEnabled)
    }

    private boolean isActionApiCorrectVersion(def action, def version) {
        Collection<ApiVersion> versionAnnotations = action.annotations.findAll {
            it instanceof ApiVersion
        }
        boolean isCorrectVersion = false
        for(versionAnnotation in versionAnnotations){
            if(versionAnnotation.value().find { it == version }){
                isCorrectVersion = true
                break
            }
        }
        return isCorrectVersion
    }

The filter first authenticates any request coming in (partial stub), then it checks whether you have access to the controller and action through the api and that the api version is supported for the given action. If all these conditions are met, then it continues to transform the model to json or xml.

Custom annotations

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEnabled {

}

This tells the ApiFilter if a given grails controller or action is allowed to output xml/json data. So if the annotation @ApiEnabled is to be found on the controller or action level, the ApiFilter will proceed with json/xml conversion

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    String[] value();
}

Im not quite sure if i need this annotation, but ill add it here for argument sake. This annotation gives information on what versions of the api this a given action supports. so if a action supports api version 0.2 and 0.3 but 0.1 has been phased out, then all requests to /api/0.1/ will fail on this action. and if i need a greater level of control on api version i can always do a simple if block or switch statement e.g:

if(params.version == '0.2'){
   //do something slightly different 
} else {
  //do the default
}

ApiMarshaller

class ApiMarshaller implements ObjectMarshaller<Converter>{

    private final static CONVERT_TO_PROPERTY = 'toAPI'

    public boolean supports(Object object) {
        return getConverterClosure(object) != null
    }

    public void marshalObject(Object object, Converter converter) throws ConverterException {
        Closure cls = getConverterClosure(object)

        try {
            Object result = cls(object)
            converter.lookupObjectMarshaller(result).marshalObject(result,converter)
        }
        catch(Throwable e) {
            throw e instanceof ConverterException ? (ConverterException)e :
                new ConverterException("Error invoking ${CONVERT_TO_PROPERTY} method of object with class " + object.getClass().getName(),e);
        }
    }

    protected Closure getConverterClosure(Object object) {
        if(object){
            def overrideClosure = object.metaClass?.getMetaMethod(CONVERT_TO_PROPERTY)?.closure
            if(!overrideClosure){
                return object.metaClass?.hasProperty(object,CONVERT_TO_PROPERTY)?.getProperty(object)
            }
            return overrideClosure
        }
        return null
    }
}

This class is registered as an objectMarshaller both for the XML and JSON converters. It checks to see if the object has an toAPI property. If so it will use this to marshal the object. the toAPI can also be overridden through MetaClass to allow another rendering strategy. (ex version 0.1 renders the object in a different way than version 0.2)

Bootstrap.. tying it all together

log.info "setting json/xml marshalling for api"

def apiMarshaller = new ApiMarshaller()

JSON.registerObjectMarshaller(apiMarshaller)
XML.registerObjectMarshaller(apiMarshaller)

This is all that needs to be done in order to make use of the new marshalling strategy.

Sample domain class

class Sample {
  String sampleText

  static toAPI = {[
    id:it.id,
    value:it.sampleText,
    version:it.version
  ]}
}

A simple domain class which shows a toAPI sample declaration

Sample controller

@ApiEnabled
class SampleController {

    static allowedMethods = [list: "GET"]

    @ApiVersion(['0.2'])
    def list = {
        def samples = Sample.list()
        return [samples:samples]
    }

}

This simple action when accessed through the api would then return a xml or json format which may or may not be defined by the Sample.toAPI(). If toAPI isnt defined, then it will use the default grails converters marshallers.

So, thats it. What do you guys think? is it flexible in accordance to my original question? Do you guys see any problems with this design or potential performance issues?

like image 110
netbrain Avatar answered Oct 21 '22 21:10

netbrain


Wait, if you still need to use the action for web UI, result still has to be a Map.

If I wished that API call returned a List, I'd add @ApiListResult('dunnoInstanceList') annotation to an action, and in API call would just take the given parameter from action result.

Or even just an @ApiListResult and pick a Map key that endsWith('InstanceList').

Versioning is going to be complex anyway, if you're going to reuse 2.0 controllers functionality to serve 1.0 requests. I'd add another couple of annotations like @Since('2.0') and, for changed signatures, @Till('1.1') and @ActionVersion('list', '1.0') def list10 = {...} - for an action that retains legacy signature.

like image 2
Victor Sergienko Avatar answered Oct 21 '22 21:10

Victor Sergienko