Umbraco REST API

Some of you may know, some might have never heard, but since June, we, as Umbraco developers, are able to use REST API without creating our own (usually achieved by using custom WebAPI controllers). Codegarden 2015 attendees were able to hear about the API during the Roadmap Q&A session and all the rest could read about it on this Umbraco blog post. Personally, I got to know about the existence of the REST API during the Umbraco UK Festival this year and Shannon's talk: "Road to V8" (slides, video).

The API is based on the HAL specification (available on Github) and is using WebApi implementation of HAL which can by also found on Github

In this article I want to go through the whole process: From installation, configuration and even a small PoC to finish with a functional prototype running on top of the current version of the Umbraco REST API. I also want to share some of my own thoughts and some of the community's thoughts about use cases, scenarios and a bright future of using this API in the near future. Let's go!

Installation

Umbraco REST API can be easily installed via NuGet:

Install-Package UmbracoCms.RestApi

It's also installing the Umbraco Identity Extensibility package which enables easy extensibility points for ASP.NET Identity and the Umbraco backoffice.

After installation, following the readme file we need to move on to the ~/App_Startup/UmbracoStandardOwinStartup.cs file and add some configuration for our API. There is a lot of useful information in the code file already, but what should interest us the most at this point is the API configuration. So we are including the code snippets present in the readme file and to begin with we are also enabling the authentication based on the backoffice cookie, just to be able to easily test the API first. Currently it is secured only based on backoffice user logins. The plan for the near future include enabling the front-end member logins and an easy approach for configuring the security during this startup configuration of the REST API.

/// <summary>
/// The standard way to configure OWIN for Umbraco
/// </summary>
/// <remarks>
/// The startup type is specified in appSettings under owin:appStartup - change it to "StandardUmbracoStartup" to use this class
/// </remarks>
public class UmbracoStandardOwinStartup : UmbracoDefaultOwinStartup
{
    public override void Configuration(IAppBuilder app)
    {
        // Ensure the default options are configured
        base.Configuration(app);

        // Configuring the Umbraco REST API options
        app.ConfigureUmbracoRestApi(new UmbracoRestApiOptions()
        {
            // Modify the CorsPolicy as required
            CorsPolicy = new CorsPolicy()
            {
                AllowAnyHeader = true,
                AllowAnyMethod = true,
                AllowAnyOrigin = true
            }
        });

        // Enabling the authentication based on Umbraco back office cookie
        app.UseUmbracoCookieAuthenticationForRestApi(ApplicationContext.Current);
    }
}

~/App_Startup/UmbracoStandardOwinStartup.cs after our modifications

As the class's comment says, we also need to change the default class used for OwinStartup to use our modified one.

<add key="owin:appStartup" value="UmbracoStandardOwinStartup" />

~/web.config key

Playground

To discover how powerful the API currently is, I've created a playground solution with Umbraco CMS v7.3.1 with Fanoe starterkit and HAL Browser HTML/JavaScript code which is a really awesome and simple tool helping to go through HAL-based APIs.

Want to play yourself? Grab a copy of the Playground from Github here.

We've got four starting endpoints to play with:

  • /umbraco/rest/v1/content
  • /umbraco/rest/v1/media
  • /umbraco/rest/v1/members
  • /umbraco/rest/v1/relations

The response body from the root of the Content section of API is shown below.

{
  "totalResults": 1,
  "_links": {
    "root": {
      "href": "/umbraco/rest/v1/content"
    },
    "search": {
      "href": "/umbraco/rest/v1/content/search{?pageIndex,pageSize,lucene}",
      "templated": true
    },
    "content": {
      "href": "/umbraco/rest/v1/content/1063"
    }
  },
  "_embedded": {
    "content": [
      {
        "templateId": 1056,
        "hasChildren": true,
        "sortOrder": 0,
        "contentTypeAlias": "Home",
        "parentId": -1,
        "writerId": 0,
        "creatorId": 0,
        "path": "-1,1063",
        "level": 1,
        "id": 1063,
        "key": "e4288a47-98a8-42ce-a5fa-7a9dcfffd1e7",
        "name": "Home",
        "createDate": "2015-11-25T01:47:36.823+00:00",
        "updateDate": "2015-11-25T01:47:37.737+00:00",
        "itemType": 0,
        "properties": {
          "siteTitle": "Fanoe - Umbraco Starter Kit",
          "siteLogo": "/media/1042/logo.jpg",
          "siteDescription": "This is an official Umbraco Starter kit. It shows of the newest features in Umbraco and is the perfect place to start for newcomers.. Everything is made to be taken apart. It's your own little playground. Enjoy!",
          "content": "{\n  \"name\": \"1 column layout\",\n  \"sections\": [\n    {\n      \"grid\": 12,\n      \"rows\": [\n        {\n          \"name\": \"Banner\",\n          \"areas\": [\n            {\n              \"grid\": 12,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"rte\",\n                \"banner_headline\",\n                \"banner_tagline\"\n              ],\n              \"config\": {},\n              \"styles\": {\n                \"margin-bottom\": \"60px\"\n              },\n              \"controls\": [\n                {\n                  \"value\": \"Welcome to Fanoe\",\n                  \"editor\": {\n                    \"name\": \"Banner Headline\",\n                    \"alias\": \"banner_headline\",\n                    \"view\": \"textstring\",\n                    \"icon\": \"icon-coin\",\n                    \"config\": {\n                      \"style\": \"font-size: 36px; line-height: 45px; font-weight: bold; text-align:center\",\n                      \"markup\": \"<h1 style='font-size:62px;text-align:center'>#value#</h1>\"\n                    }\n                  }\n                },\n                {\n                  \"value\": \"This is the new default starter kit for Umbraco, we enjoy building things that not only look great, but also work great.\",\n                  \"editor\": {\n                    \"name\": \"Banner Tagline\",\n                    \"alias\": \"banner_tagline\",\n                    \"view\": \"textstring\",\n                    \"icon\": \"icon-coin\",\n                    \"config\": {\n                      \"style\": \"font-size: 25px; line-height: 35px; font-weight: normal; text-align:center\",\n                      \"markup\": \"<h2 style='font-weight: 100; font-size: 40px;text-align:center'>#value#</h2>\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"grid\": 4,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"rte\",\n                \"media\"\n              ],\n              \"controls\": [\n                {\n                  \"value\": \"<h2>The starter kit</h2>\\n<p>Congratulations on getting up and running with the “Fanoe” starter kit. It's a great way to get to know the Grid datatype.</p>\\n<p><a href=\\\"/{localLink:1078}\\\" title=\\\"The starter kit\\\">Learn about the starter kit →</a><a href=\\\"/{localLink:1078}\\\" title=\\\"The Starterkit\\\"><br /></a></p>\",\n                  \"editor\": {\n                    \"name\": \"Rich text editor\",\n                    \"alias\": \"rte\",\n                    \"view\": \"rte\",\n                    \"icon\": \"icon-article\"\n                  }\n                }\n              ]\n            },\n            {\n              \"grid\": 4,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"rte\",\n                \"media\"\n              ],\n              \"controls\": [\n                {\n                  \"value\": \"<h2>Learn the basics</h2>\\n<p>To get started on the right foot make sure to check out our documentation - from tutorials to API reference.</p>\\n<p><a href=\\\"/{localLink:1079}\\\" title=\\\"Basics\\\">Off to the docs →</a></p>\",\n                  \"editor\": {\n                    \"name\": \"Rich text editor\",\n                    \"alias\": \"rte\",\n                    \"view\": \"rte\",\n                    \"icon\": \"icon-article\"\n                  }\n                }\n              ]\n            },\n            {\n              \"grid\": 4,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"rte\",\n                \"media\"\n              ],\n              \"controls\": [\n                {\n                  \"value\": \"<h2>Master classes</h2>\\n<p>The best way to learn Umbraco is through the Master classes that’s running every month in most regions.</p>\\n<p><a href=\\\"/{localLink:1080}\\\" title=\\\"Masterclasses\\\">Learn about the masterclasses →</a></p>\",\n                  \"editor\": {\n                    \"name\": \"Rich text editor\",\n                    \"alias\": \"rte\",\n                    \"view\": \"rte\",\n                    \"icon\": \"icon-article\"\n                  }\n                }\n              ]\n            }\n          ],\n          \"styles\": {\n            \"background-image\": \"url(/media/1044/14441035391_7ee1d0d166_h_darken.jpg)\"\n          },\n          \"config\": {\n            \"class\": \"dark\"\n          },\n          \"id\": \"d28a05da-2404-c1e7-6265-01b07fc5f722\"\n        },\n        {\n          \"name\": \"Two column\",\n          \"areas\": [\n            {\n              \"grid\": 6,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"rte\",\n                \"media\",\n                \"media_round\",\n                \"quote\"\n              ],\n              \"controls\": [\n                {\n                  \"value\": \"<h1>Umbraco Forms</h1>\\n<p>With our add-on product Umbraco Forms it’s a breeze to create any type of form you can imagine.</p>\",\n                  \"editor\": {\n                    \"name\": \"Rich text editor\",\n                    \"alias\": \"rte\",\n                    \"view\": \"rte\",\n                    \"icon\": \"icon-article\"\n                  }\n                }\n              ]\n            },\n            {\n              \"grid\": 6,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"rte\",\n                \"media\",\n                \"media_round\",\n                \"quote\"\n              ],\n              \"controls\": [\n                {\n                  \"value\": \"<h1>Package repository</h1>\\n<p>At Our Umbraco you’ll find hundreds of free packages made and maintained by the community.</p>\",\n                  \"editor\": {\n                    \"name\": \"Rich text editor\",\n                    \"alias\": \"rte\",\n                    \"view\": \"rte\",\n                    \"icon\": \"icon-article\"\n                  }\n                }\n              ]\n            }\n          ],\n          \"styles\": {\n            \"background-image\": \"url(/media/1023/form-bg.png)\"\n          },\n          \"config\": {\n            \"class\": \"light\"\n          },\n          \"id\": \"5362ea7d-e729-078b-e717-a1e7aa69fd4b\"\n        },\n        {\n          \"name\": \"Full width image\",\n          \"areas\": [\n            {\n              \"grid\": 12,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"media_wide_cropped\",\n                \"media_wide\"\n              ],\n              \"controls\": [\n                {\n                  \"value\": {\n                    \"focalPoint\": {\n                      \"left\": 0.5,\n                      \"top\": 0.5\n                    },\n                    \"id\": 1127,\n                    \"image\": \"/media/1046/14386124825_d43f359900_h_cropped.jpg\"\n                  },\n                  \"editor\": {\n                    \"name\": \"Image wide\",\n                    \"alias\": \"media_wide\",\n                    \"view\": \"media\",\n                    \"render\": \"/App_Plugins/Grid/Editors/Render/media_wide.cshtml\",\n                    \"icon\": \"icon-picture\"\n                  }\n                }\n              ]\n            }\n          ],\n          \"styles\": {},\n          \"config\": {\n            \"class\": \"full triangle\"\n          },\n          \"id\": \"953b3802-6b4f-20ed-66f9-9a855e9afdd4\"\n        },\n        {\n          \"name\": \"Two column\",\n          \"areas\": [\n            {\n              \"grid\": 6,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"rte\",\n                \"media\",\n                \"media_round\",\n                \"quote\"\n              ],\n              \"controls\": [\n                {\n                  \"value\": \"<h2>Sharing is caring</h2>\\n<p>Our Umbraco is our community hub, bringing together over a hundred thousand people every month sharing knowledge in the forum, documentation, blogs, packages and much more.</p>\",\n                  \"editor\": {\n                    \"name\": \"Rich text editor\",\n                    \"alias\": \"rte\",\n                    \"view\": \"rte\",\n                    \"icon\": \"icon-article\"\n                  }\n                }\n              ]\n            },\n            {\n              \"grid\": 6,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"rte\",\n                \"media\",\n                \"media_round\",\n                \"quote\"\n              ],\n              \"controls\": [\n                {\n                  \"value\": \"<h2>Born global, loving local</h2>\\n<p>If you can’t make it to CodeGarden, that doesn’t mean you have to miss the chance of hanging out with the Umbraco community. You can do just that through the many local user groups and meetups</p>\",\n                  \"editor\": {\n                    \"name\": \"Rich text editor\",\n                    \"alias\": \"rte\",\n                    \"view\": \"rte\",\n                    \"icon\": \"icon-article\"\n                  }\n                }\n              ]\n            }\n          ],\n          \"styles\": {},\n          \"config\": {\n            \"class\": \"purple\"\n          },\n          \"id\": \"29cac0ff-d8e9-605b-9e7b-6809dce55edd\"\n        },\n        {\n          \"name\": \"Banner\",\n          \"areas\": [\n            {\n              \"grid\": 12,\n              \"allowAll\": false,\n              \"allowed\": [\n                \"rte\",\n                \"banner_headline\",\n                \"banner_tagline\"\n              ],\n              \"controls\": [\n                {\n                  \"value\": \"<h1 style=\\\"text-align: center;\\\">CodeGarden</h1>\\n<h5 style=\\\"text-align: center;\\\">Every year in June, around 500 Umbracians gather in Denmark for our annual conference “CodeGarden”.</h5>\",\n                  \"editor\": {\n                    \"name\": \"Rich text editor\",\n                    \"alias\": \"rte\",\n                    \"view\": \"rte\",\n                    \"icon\": \"icon-article\"\n                  }\n                }\n              ]\n            }\n          ],\n          \"styles\": {\n            \"background-image\": \"url(/media/1028/14435732345_c9f5b48dfa_h_dark.jpg)\"\n          },\n          \"config\": {\n            \"class\": \"dark\"\n          },\n          \"id\": \"0093bd05-735a-9623-8509-28085818745c\"\n        }\n      ]\n    }\n  ]\n}"
        },
        "_links": {
          "self": {
            "href": "/umbraco/rest/v1/content/1063"
          },
          "root": {
            "href": "/umbraco/rest/v1/content"
          },
          "meta": {
            "href": "/umbraco/rest/v1/content/1063/meta"
          },
          "children": {
            "href": "/umbraco/rest/v1/content/1063/children{?pageIndex,pageSize}",
            "templated": true
          },
          "descendants": {
            "href": "/umbraco/rest/v1/content/1063/descendants{?pageIndex,pageSize}",
            "templated": true
          },
          "upload": {
            "href": "/umbraco/rest/v1/content/1063/upload"
          }
        }
      }
    ]
  }
}

It has only one property - totalResults - which is showing how many root nodes are present in the Content section of our backoffice. According to Hypertext Application Language specification it also has additional properties for the HAL Model:

  • _links for Links (to documents URIs)
  • _embedded for Embedded Resources (i.e. other resources contained within the item)

Our embedded items have all the standard JSON properties listed inside of them and of course the HAL Model properties helping to go throught them to discover other items below them for example.

HAL representation structure
HAL representation structure

We can easily play with all the items, navigate through them and find or create all what we need. All the sections are working and returning data in the same way.

Possible use cases

So where will it be helpful? Everywhere where REST is required or needed.

We can use it inside our application, enabling users or members to manage parts of Umbraco outside the backoffice panel and without any knowledge of C# for example. All the scenarios where e.g. SPAs (single page apps) or landing pages may be created by other developers/companies who may have skills or ability to use or create their own items directly from front-end code, without interrupting the structure of the ASP.NET project.

We can also use the API in any external, 3rd party app (mobile too!). It could be used to integrate Umbraco with other systems; CRMs, Productivity tools (IFTTT, Slack, Trello maybe?) or by custom apps using content or data from our Umbraco instances. For example, we've been thinking about creating mobile apps to help manage content mainly for our Clients. With REST API and its presence in each Umbraco installation for example, it could be simpler than we thought. It won't be necessary to use any other, additional stuff for that and the app may be universal for all of us (Challenge accepted?! :)).

The other options? It may be all the rest of what REST is for :)

PoC

I wanted to dig a little bit more and create small proof-of-concept showing that we really can access our Umbraco data from any other app. 

This is also a great time to learn something new, so I decided to finally take a look at the Ionic framework and Restangular.

Let's assume that we want to create a simple Ionic app listing the root content nodes from an Umbraco instance.

I found a great sample with pull-refresh list in Ionic: http://codepen.io/ionic/pen/mqolp, exported project as .zip file and created a separate website in my Playground solution.

The main concern is of course the security and accessing the data from another app. Backoffice cookie is no longer an option here, so we need to switch to another authentication method - token-based authentication (or enable it as additional option).

According to docs and what we learned earlier, we just need to go to our appStartup class and add one line:

app.UseUmbracoBackOfficeTokenAuth();

I decided to create a separate user, called "API", which has access to "Content", "Media" and "Member" sections only. I will be authenticating and accessing the token for this particular user. For this purpose I created an Angular service taking care of getting the token by creating a POST request to authentication endpoint enabled after setting usage of backoffice token auth.

// Authentication token url
var authUrl = 'http://localhost:55368/umbraco/oauth/token';

// Authentication service
ionicApp.factory('authResource', function ($http) {

    var getAccessToken = function (grantType, username, password) {

        var postData = {
            grant_type: grantType,
            username: username,
            password: password
        };

        return $http.post(authUrl, postData, {
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            transformRequest: function (obj) {
                var str = [];
                for (var p in obj)
                    if (obj.hasOwnProperty(p)) str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
                return str.join("&");
            }
        }).then(function (response) {
            return response.data;
        });

    };

    return {
        getAccessToken: getAccessToken
    }
});

Angular authentication service

We need to make sure to not expose the user credentials in files which are easily accessed from the client side of our app.

Quoting the REST API documentation:
"Since the REST API is currently only secured against backoffice user logins it is important to understand that if you want to use the REST API for applications that exist outside of the web domain, that you do not hard code backoffice user login credentials. In the future when the security for the REST API can be more flexible it would be advised to authenticate your apps based on a member's credentials that your app users would need to enter."

As this is just a PoC, credentials will be exposed, but just keep in mind that this is not a best practice and not advised at all :)

Now we are able to receive a token which we need to attach to each following request to the Umbraco REST API.

After playing around with requesting data throught the $http service, I decided to find an easier way to handle API and HAL responses. I found a couple of possible solutions to achieve this:

and chose Restangular (it was easier to understand for me, offering many custom options and methods and it looks very promising).

To avoid duplicating a lot of code required to properly construct all the requests to our API, I created a global configuration for Restangular.

// Restangular configuration
ionicApp.config(function (RestangularProvider) {
    // Set the base url for your API endpoints
    RestangularProvider.setBaseUrl(apiBase);

    // You can set some default headers for calling the API
    RestangularProvider.setDefaultHeaders(
        { 'Accept': 'application/hal+json' },
        { 'Content-Type': 'application/hal+json' }
    );

    // Set an interceptor in order to parse the API response 
    // when getting a list of resources
    RestangularProvider.setResponseInterceptor(function (data, operation, what) {
        if (operation == 'getList') {
            resp = data._embedded[what];
            resp._links = data._links;
            return resp;
        }
        return data;
    });

    // Using self link for self reference resources
    RestangularProvider.setRestangularFields({
        selfLink: 'self.link'
    });
});

Restangular global configuration

Using the options from Restangular interceptors we can also create an error interceptor for example which can be responsible for refreshing and getting a new token if, for example, our current one expires.

The last step was to create a service responsible for getting the data from our API endpoint.

// Content service - very basic!
ionicApp.factory('contentResource', function ($http, Restangular) {

    var getRootItems = function (authorization) {

        return Restangular.all("content").getList({}, { 'Authorization': authorization });

    }

    return {
        getRootItems: getRootItems
    }

});

Angular content service

Looks really clean, doesn't it?

Putting it all together we are finally able to retrieve a token and get the root items from Umbraco.

$scope.init = function () {

    // Get access token for back office user (can be different for different apps) - Temporary
    // Probably will be deprecated regarding to plans of changing the authentication method(s).
    // Remember to not expose user credentials in production apps!!! This is just PoC.
    var accessTokenResponse = authResource.getAccessToken('password', 'api', 'password');
    accessTokenResponse.then(function (response) {
        $scope.tokenResponse = response;

        console.log($scope.tokenResponse);

        $scope.loadRootItems();
    });
};

$scope.loadRootItems = function () {

    // Get root items with passed authorization details to authenticate the request
    var rootItems = contentResource.getRootItems($scope.tokenResponse.token_type + ' ' + $scope.tokenResponse.access_token);
    rootItems.then(function (response) {
        console.log(response);

        $scope.items = response;
    });

};

Angular init and load root items

Let's test it by adding another Home node after retrieving the data for the first time.

Fanoe default Content section
Fanoe default Content section
Ionic App - first request
Ionic App - first request
Adding new root node to Starter Kit
Adding new root node to Starter Kit
Ionic pull-refresh
Ionic pull-refresh

Boom! It works! :) Now we can go deeper and deeper, playing, trying, exploring. The code of the PoC is also inside the Github repository here. Feel free to explore it, extend it and share your feedback and thoughts.

Summary

The Umbraco REST API, even at the current stage, is a great piece of work and starter point to extend the Umbraco usage for everyone. It's not perfect (yet!) and requires some additional work to setup and include inside the package, but hey... it's not even full v1.0.0.

We can connect to our Umbraco websites, authenticate users from 3rd party systems and manipulate data without using the backoffice. The rest is only limited by our imagination, time and budgets :)

From my little research, users are extending the API to enable e.g.:

  • publish options for nodes
  • delete action for nodes
  • authenticate users without having back office accounts

Looking forward to the next steps from the Core Team and community as well. The result of this work will be a great piece of code/tool and a "must-have" for anyone interested in using REST with their solutions.

 

Sources:
https://our.umbraco.org/documentation/Implementation/Rest-Api/
https://github.com/umbraco/UmbracoRestApi
http://www.javifernandez.me/2014/12/21/consuming-hypermedia-apis-with-restangular/

Marcin Zajkowski

Marcin is on Twitter as