One API to Build Them All One API to Build Them All

TL;DR


Nuxeo is a pluggable platform and when we decided to expose a new document oriented API we had to think about its pluggability. We addressed it by making the content of a response pluggable, by making calls composable and allowing our users to add their own endpoints. Everything is done so that our users will be able to build their own API on top of Nuxeo's.

A Bit of History


Nuxeo is from the beginning based on the JavaEE stack, and makes use of JSF and Seam for the UI which makes it a rather stateful platform even if we have meaningful URLs.

Thanks to Restlets we were able to provide some REST endpoints but they were mostly used to GET information.

Some time after, we introduced WebEngine which was a web framework based on JAX-RS and with which you could do some REST APIs. The way we used it was to expose what we call Automation Operations that you could execute by sending a POST to them and put parameters in the body… But wait, this is like RPC! Yes indeed! But this is composable RPC since you can chain several operations to make an Automation Chain that is exposed through the same mechanism. The way to chain operations is to drag'n'drop operations in Nuxeo Studio, but that's another story.

So, Nuxeo was missing a real REST API where we could expose our first class resources (i.e. documents) and that our customers could use to expose their own APIs.

When It Comes to Pluggability


Nuxeo is designed to be a platform where our customers can customize many things. So the API also has to be customized and pluggable at several levels. Ok, so what does it mean for an API to be pluggable?

Let's take a simple example based on an application that just browses the content repository. When showing a document, it will have to call the current document properties. This looks like a GET and indeed here is what you have by calling the document endpoint:

GET /nuxeo/api/v1/path/default-domain/workspaces/
{
"entity-type": "document",
"repository": "default",
"uid": "b1608191-cd5e-4345-8437-d17451585876",
"path": "/default-domain/workspaces",
"type": "WorkspaceRoot",
"state": "project",
"versionLabel": "",
"title": "Workspaces",
"lastModified": "2013-09-25T15:51:54.43Z",
"properties": {
"dc:creator": "system",
"dc:source": null,
"dc:nature": null,
"dc:contributors": [
"system",
"Administrator"
],
"dc:created": "2013-09-16T15:52:12.91Z",
...
"dc:modified": "2013-09-25T15:51:54.43Z",
"dc:title": "Workspaces",
"dc:lastContributor": "Administrator"
},
"facets": [
"SuperSpace",
"Folderish"
],
"changeToken": "1380124314434",
"contextParameters": {}
}

The returned JSON object contains the properties of the document which is sufficient in many cases. But let's focus on the other cases.

I Don't Want All That Data!


In the resulting JSON, we can see a lot of technical data that can be very interesting from a technical point of view but that may be useless for your front end application -- you just want your business data. In Nuxeo, the way to expose business code is by using adapters. It's something that adapts or wraps a DocumentModel and exposes some business methods as a bean.

To expose that in your API, it's actually quite simple: you have to use the BusinessObject adapter just by adding /@bo/MyBusinessAdapter. Then you will receive your bean serialized as JSON.

GET /nuxeo/api/v1/path/default-domain/workspaces/@bo/TitleDescription
{
"type": "WorkspaceRoot",
"id": "b1608191-cd5e-4345-8437-d17451585876",
"title": "Workspaces",
"description": "Workspaces"
}

I Need More Data!


In a given GET, we have seen you get a lot of technical data on the document itself but that's it. If I want to get the children of a document, I can make a call to {documentUrl}/@children. But if I also want the ancestors to build a breadcrumb, I will have to query each part of the document's path to get the title of its ancestors. That can make a lot of calls!

That's why we introduced what we call context parameters. At the end of the document's map, you perhaps noticed that there was a contextParams property that was empty. This map may be used for that: add some contextual data. We have a header that is used to trigger the population of the map. Basically it's based on a category and you can plug what we call a RestContributor that will write its own data.

Here are two examples of what it can be used for:

When you upload a picture, Nuxeo computes additional views that you can refer to in the resulting JSON.

GET /nuxeo/api/v1/path/default-domain/workspaces/album/picture1
{
"entity-type": "document",
"type": "Picture",
"state": "project",
...
"contextParameters": {
"pictureViews": {
"thumbnail": {
"size": "50x50",
"url":"/nuxeo/a … spaces/album/picture1/@views/thumbnail"
},
"Medium": {
"size": "400x200",
"url":"/nuxeo/a … spaces/album/picture1/@views/Medium"
}
}
}
}

When you're on a document, some actions are available to the user. You can add those actions with their label and their URL.

GET /nuxeo/api/v1/path/default-domain/workspaces/album/picture1

{
"entity-type": "document",
"type": "Picture",
"state": "project",
...
"contextParameters": {
"actions": {
"rotateCW": {
"label":"Rotate clockwise",
"icon":"/icons/rotateCW.png"
"url":"/nuxeo/a … spaces/album/picture1/@op/Picture.rotateCW"
},
"rotateCCW": {
"label": "Rotate counter clockwise",
"icon": "/icons/rotateCCW.png"
"url":"/nuxeo/a … spaces/album/picture1/@op/Picture.rotateCCW"
}
}
}
}


When you show a document, some properties are just identifiers like authors and contributors. You can use the context to expand those identifiers by introspecting the resource's properties:

GET /nuxeo/api/v1/path/default-domain/workspaces/album/picture1

{
"entity-type": "document",
"type": "Picture",
"state": "project",
"properties": {
"dc:creator": "jdoe",
"dc:contributors": [
"jdoe"
],
...
"dc:lastContributor": "jdoe"
},
...
"contextParameters": {
"expandedUsers": {
"jdoe": {
"fullName":"John Doe",
"url":"/nuxeo/api/user/jdoe"
}
}
}
}


There are dozens of other examples where context parameters may be used and this is how we would deal with some HATEOAS paradigms: links to other resources may be found in this map.

The only thing to do to populate this map is to write a contributor that will react to a context.

I Want to Run Operations on My Resources


Remember now that Nuxeo offers what we call automation chains/operations. Those operations often run on a document or list of documents. We wanted to find an easy way to run those operations.

That's why we introduced adapters that enable resource composition (the already seen business object adapter is one of those). When you point to a resource, you can compose that resource with an adapter that exposes another facet of the resource.

For instance, to run an operation on a document, you point to the document and adapt it with an @op adapter:

POST /nuxeo/api/v1/path/default-domain/.../myDocument/@op/convertToPDF

Since resources are composable you can chain adapters. For instance to make a ZIP of all documents of a folder:

POST /nuxeo/api/v1/path/default-domain/.../myFolder/@children/@op/makeAZIP

So, when you bind resource composition and the fact that you are able to define your own operation chain with a simple drag'n'drop tool, this enables you to expose a lot of things in your REST API.

I Need My Own Endpoints


By default, Nuxeo provides a limited set of endpoints:


  • documents by path

  • documents by id

  • users

  • groups


Perhaps one needs to add some more business oriented endpoints, like for a retail application:


  • products

  • suppliers



Thanks to WebEngine and its pluggability, we provided a way to add other root endpoints that benefit from the same infrastructure. It's just a matter of providing a web object in another bundle like this:
@WebObject(type="productRoot")
public class ProductRoot {

@GET
public List getProducts() throws ClientException {
ProductService ps = Framework.getLocalService(ProductService.class);
return ps.getAllProducts(getContext().getCoreSession());
}

@Path("{productId}")
public Object getProduct(@PathParam("productId") String productId) {
return newObject("product", productId);
}

}


This will expose two new APIs:


GET /nuxeo/api/v1/product
GET /nuxeo/api/v1/product/{productId}

Since DocumentAdapters are used as return types, the API will automatically benefit from the integrated adapter serializations (readers and writers). That means that it is very easy to build your own API, and that you inherit all the other pluggability mechanisms.

Conclusion: One API to Build Them All


As we think Nuxeo is a great pluggable platform, we thought our API must be as pluggable as the platform. I personally think that a REST API is just another type of UI. In Nuxeo UI you can customize the UI by adding actions and information. We tried to give the same level of pluggability to our API so that our customers may build their own API.

One platform to build them all must also provide an API to build them all.