Overview

Setting up a Services server is discussed elsewhere. We will call our sandbox server http://services.example.com.

It is important that you understand REST concepts. The Drupal Services REST server follows the REST style closely, and you will minimize confusion if you understand why things are done the way they are. See Representational state transfer and RESTful Web services: The basics for a good introduction.

Then read Working with REST Server for a rather terse discussion of how Drupal Services implements REST.


Drupal Services Uses drupal_get_form()

Drupal Services uses drupal_get_form() wherever it can. This means that it accepts the same fields, with the same names, as the forms you see in your browser on a Drupal site. This approach has many advantages:

  • Familiarity
  • Using existing code
  • Allowing all hooks to run, so other modules that work with forms can run just as they do via the browser interface.

There is at least one significant disadvantage to this approach. If you change the fields in a content type, even just the widget, you may have to change the JSON/REST requests you make via Services. In other words, changing a content type may break your services interface.


Requirements For a Successful JSON/REST call

  • Correct URL
  • Correct action: POST, GET, PUT
  • Correct content type: application/json for this discussion
  • Correct JSON
    • Valid JSON format. See www.json.org for the JSON spec.
    • Correct field names
    • Correct values
  • Correct session status
    • Logged in or not, as the situation calls for.
    • Cookie or not, as the situation calls for.
  • Correct server configuration - must enable and permit the desired function.


The HttpRequester Tool

I suggest using the Firefox addon HttpRequester for accessing your sandbox. It is simple to install and use and allows you to focus on the essentials of the JSON/REST activity.

There is another great tool that you can use Postman - REST Client

Here is a short of its features:

  • Compact layout
  • HTTP requests with file upload support
  • Formatted API responses for JSON and XML
  • HATEOS support
  • Image previews
  • Request history
  • Basic Auth and OAuth 1.0 helpers
  • Autocomplete for URL and header values
  • Key/value editors for adding parameters or header values. Works for URL parameters too.
  • Use environment variables to easily shift between settings. Great for testing production, staging or local setups.
  • Keyboard shortcuts to maximize your productivity

Which is also available as a chrome extension https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnp...

HttpRequester has URL, content type, and content fields that precisely match the examples shown below.

However, you can use any interface you prefer, such as a CURL script, an app you are developing, or whatever you like.

One 'gotcha' when using HttpRequester - your HttpRequester session status is shared with the browser. Logging in or out via the browser has the same effect in your HttpRequester session, and vice versa. This is a bug or a feature, depending on what you want to do. During development, I establish a browser session with the server via Chrome, and use the Firefox session only for HttpRequester. This keeps the browser and HttpRequester sessions separate.


Examples

These are URLs that work more or less 'out of the box'. That is, they need configuration and permissions, but no custom coding.

To use these examples with HttpRequester:

  • Copy the URL (the part starting with 'HTTP') into the URL text field.
  • Choose the 'application/json' Content Type from the dropdown menu.
  • Copy the JSON (the braces and everything within them) into the content textarea. Some of the examples do not have any JSON, in that case the content textarea remains empty.
  • Click the GET, POST, or PUT button as appropriate for the example.

We show the 'View raw transaction' output from HttpRequester, with the header info edited out for brevity. For the more complex responses, you can run the JSON part of the response through the JSON formatter at Curious Concept JSON Formatter to improve readability.

After adding or altering a node, you can visit your server in your browser to see the results.

I will be adding more examples. If you have your own examples of 'out of the box' functions, please add them as comments and I will work them into the body.


Register a new user

POST http://services.example.com/rest/user/register
    Content-Type: application/json
    {
        "name":"services_user_1",
        "pass":"password",
        "mail":"services_user_1@example.com"
    }

     -- response --
    200 OK
    Content-Type:  application/json
    {"uid":"2","uri":"http://services.example.com/rest/user/2"}

Our server is configured to allow visitors to register themselves without an email, so this leaves the new user logged in to the site.


Create an Article

POST http://services.example.com/rest/node
Content-Type: application/json
{
  "type":"article",
  "title":"Article submitted via JSON REST",
  "body":{
    "und":[
      {
        "value":"This is the body of the article."
      }
    ]
  }
}

 -- response --
200 OK
{"nid":"22","uri":"http://services.example.com/rest/node/22"}

The 'nid' is the node id of the node you just created, and the uri can be used to retrieve the node.


Warning: 200 Response Code Is Not Enough

200 OK - But It Isn't OK

POST http://services.example.com/rest/node
Content-Type: application/json
{
  "type":"article",
  "title":"Article With No Body",
  "body":{
    "und":[
      {
        "body":"This article body will not be created."
      }
    ]
  }
}

 -- response --
200 OK
{"nid":"27","uri":"http://services.example.com/rest/node/27"}

This request is very similar to the previous example, but it uses "body":"This article body will not be created." when it should use "value":"This article body will not be created.". The request looks like it succeeds, and the article has been created, but the article body is NOT created. You have to test your procedures and make sure they work.


Retrieve an Article

GET http://services.example.com/rest/node/22


 -- response --
200 OK
{"vid":"22","uid":"11","title":"Article submitted via JSON REST","log":"","status":"1","comment":"2","promote":"1","sticky":"0","nid":"22","type":"article","language":"en","created":"1329691988","changed":"1329691988","tnid":"0","translate":"0","revision_timestamp":"1329691988","revision_uid":"11","body":{"und":[{"value":"This is the body of the article.","summary":"","format":"filtered_html","safe_value":"<p>This is the body of the article.</p>\n","safe_summary":""}]},"field_tags":[],"field_image":[],"rdf_mapping":{"field_image":{"predicates":["og:image","rdfs:seeAlso"],"type":"rel"},"field_tags":{"predicates":["dc:subject"],"type":"rel"},"rdftype":["sioc:Item","foaf:Document"],"title":{"predicates":["dc:title"]},"created":{"predicates":["dc:date","dc:created"],"datatype":"xsd:dateTime","callback":"date_iso8601"},"changed":{"predicates":["dc:modified"],"datatype":"xsd:dateTime","callback":"date_iso8601"},"body":{"predicates":["content:encoded"]},"uid":{"predicates":["sioc:has_creator"],"type":"rel"},"name":{"predicates":["foaf:name"]},"comment_count":{"predicates":["sioc:num_replies"],"datatype":"xsd:integer"},"last_activity":{"predicates":["sioc:last_activity_date"],"datatype":"xsd:dateTime","callback":"date_iso8601"}},"cid":"0","last_comment_timestamp":"1329691988","last_comment_name":null,"last_comment_uid":"11","comment_count":"0","name":"services_user_1","picture":"0","data":null,"path":"http://services.example.com/node/22"}

Note that the URL in this case is the one that was returned by the previous request, when the article was created. Drupal Services returns a URL to the created resource whenever possible.


Comment On an Article

POST http://services.example.com/rest/comment
Content-Type: application/json
{
  "nid":28,
  "subject":"Comment submitted via JSON REST",
  "comment_body":{
    "und":[
      {
        "value":"This is a great article."
      }
    ]
  }
}


 -- response --
200 OK
{"cid":"1","uri":"http://services.example.com/rest/comment/1"}

You need to know the nid (node id) of the article to add a comment. This may or may not be be in the URL of the article.


Create a Page

POST http://services.example.com/rest/node
Content-Type: application/json
{
  "type":"page",
  "title":"Page submitted via JSON REST",
  "body":{
    "und":[
      {
        "value":"This is the body of the page."
      }
    ]
  }
}


 -- response --
200 OK
{"nid":"24","uri":"http://services.example.com/rest/node/24"}

Note that the page is created, but does not show up in a menu on the site. Adding the menu settings is quite a bit more complicated. However, if you configure the Basic Page content type to be shown in the list of new nodes on the front page, you can see the page there.


Alter an Article

PUT http://services.example.com/rest/node/22
Content-Type: application/json
{
    "type":"article",
    "title":"Change the Title of an Article"
}


 -- response --
200 OK
{"nid":"22","uri":"http://services.example.com/rest/node/22"}

Note that this is a PUT. We are updating an existing resource, in the REST approach this requires a PUT.


Get All Nodes

GET http://services.example.com/rest/node


 -- response --
200 OK
[{"nid":"27","vid":"27","type":"article","language":"en","title":"Article With No Body","uid":"11","status":"1","created":"1329692914","changed":"1329692914","comment":"2","promote":"1","sticky":"0","tnid":"0","translate":"0","uri":"http://services.example.com/rest/node/27"},{"nid":"26","vid":"26","type":"article","language":"en","title":"Article submitted via JSON REST","uid":"11","status":"1","created":"1329692842","changed":"1329692842","comment":"2","promote":"1","sticky":"0","tnid":"0","translate":"0","uri":"http://services.example.com/rest/node/26"},{"nid":"24","vid":"24","type":"page","language":"en","title":"Page submitted via JSON REST","uid":"11","status":"1","created":"1329692316","changed":"1329692316","comment":"1","promote":"1","sticky":"0","tnid":"0","translate":"0","uri":"http://services.example.com/rest/node/24"},{"nid":"22","vid":"22","type":"article","language":"en","title":"Change the Title of an Article","uid":"11","status":"1","created":"1329691988","changed":"1329692495","comment":"2","promote":"1","sticky":"0","tnid":"0","translate":"0","uri":"http://services.example.com/rest/node/22"},{"nid":"4","vid":"4","type":"page","language":"und","title":"Sandbox Server Setup","uid":"1","status":"1","created":"1328999413","changed":"1329591136","comment":"1","promote":"0","sticky":"0","tnid":"0","translate":"0","uri":"http://services.example.com/rest/node/4"}]


Logout a user

POST http://services.example.com/rest/user/logout
    Content-Type: application/json


     -- response --
    200 OK
    Content-Type:  application/json
    true

This is a POST, even though there is no JSON content, because it changes the state of the server.


JSON structure for Drupal Form Fields

You can figure out the required structure of the JSON request by looking at the form for the action you want to do.

For example, to add an article, navigate in your browser to node/add/article and examine the source code of the form. Find the fields you want to fill in. (I've edited out all extraneous material):

  • &lt;input type="text" name="title" value="" /&gt;
  • &lt;textarea name="body[und][0][value]" &gt;

The 'title' field is easy. The corresponding JSON is

    "title":"Title Text Goes Here",

The 'body' field is not so simple. body[und][0][value] indicates an array that the Drupal form API is using to store the body and related information. For example, the [und] component indicates the language ('und' corresponds to LANGUAGE_NONE). There are other parts of the array that we can ignore.

Here is a no-guessing way to get from body[und][0][value] to a JSON expression. Write a little PHP script that expresses body[und][0][value] as an array, then json_encode it. Here's a complete script that creates the whole expression.:


$request = array(
  'type' => 'article',
  'title' => 'Article submitted via JSON REST services',
  // body[und][0][value]
  'body' => array('und' => array( 0 => array('value' => 'This is the body of the article.'))),
);

echo json_encode($request);

Here is the output of the script:

{"type":"article","title":"Article submitted via JSON REST services","body":{"und":[{"value":"This is the body of the article."}]}}

Note that our JSON request includes the "type":"article" field although the form did not have one. The form actually does include this information, however, in the action="/node/add/article" parameter of the form tag.


JSON/REST Error Codes and How to Interpret Them

I'm skipping obvious errors, like:

    401 Unauthorized: Wrong username or password.
    406 Not Acceptable: E-mail address field is required.
    406 Not Acceptable: The e-mail address &lt;em class="placeholder">services_user_1@example.com&lt;/em> is already registered. &lt;a href="/user/password">Have you forgotten your password?&lt;/a>

Less obvious errors

0

POST http://services.example.com/rest/comment
Content-Type: application/json
{
  "nid":28,
  "subject":"Comment submitted via JSON REST",
  "comment_body":{
    "und":[
      {
        "value":"This is a great article."
      }
    ]
  }
}


 -- response --
0

The URL is missing the .com. The request never reaches the server.

404 Not found: Could not find resource register.

    POST http://services.example.com/rest/register
        404 Not found: Could not find resource register.

The URL should be rest/user/register, not rest/register.

406 Not Acceptable: Unsupported request content type text/xml

    406 Not Acceptable: Unsupported request content type text/xml

Should be Content-Type: application/json

401 Unauthorized: Missing required argument node

POST http://services.example.com/rest/node
Content-Type: application/json
{
  "type":"page",
  "title":"Page submitted via JSON and REST services",
  "body":{
    "und":[
      {
        "value":"This is the body of the page."
      }
    ]
  }


 -- response --
401 Unauthorized: Missing required argument node

This unhelpful message is occurring because of invalid JSON - the closing brace is missing.

401 Unauthorized: Access denied for user test_user_2

    POST http://services.example.com/rest/user/register
        Content-Type: application/json
        {
            "name":"services_user_6",
            "pass":"test_password",
            "mail":"services_user_6@example.com"
        }

         -- response --
        401 Unauthorized: Access denied for user test_user_2
        null

Can't register a new user while already logged in.

401 Unauthorized: Access denied for user services_user_6

    POST http://services.example.com/rest/node
      Content-Type: application/json
      {
          "type":"article",
          "title":"Article submitted via Services",
          "body":"Here is the body of an article submitted via Services."
      }

       -- response --
      401 Unauthorized: Access denied for user services_user_6

The server has not allowed the 'Article: Create new content' permission for authenticated users.

404 Not found: Could not find the controller.

POST http://services.example.com/rest/node/3
Content-Type: application/json
{
    "type":"page",
    "title":"Basic Page submitted via Services",
    "body":"Here is the body of a page submitted via Services."
}

 -- response --
404 Not found: Could not find the controller.

This is not a proper REST request and does not make sense to Drupal Services. By using POST with node/3, the request is, in effect, asking to create a node within a node, something Drupal Services does not have a method for, hence the 404.

406 Not Acceptable: Missing node type

PUT http://services.example.com/rest/node/3
Content-Type: application/json
{
    "title":"Change the Title of a Basic Page"
}

 -- response --
406 Not Acceptable: Missing node type
null

The node type field is required here. This is reasonable but not intuitively obvious.

Comments

ybthefurste’s picture

Hierarchical Select has its own peculiarity when creating node using Services. Format is, of course, different in D7/Services 3 than it was in D6/Services 2, and I failed to find online documentation or anyone who has posted the input format. I did figure it out, so for anyone having trouble creating a node on a RESTful server, here is the JSON format for a sample post:

{
"body":"","type":"teahouse","title":"TestingTitle","uid":"0",
"locations":[{"city":"Kalamazoo"}],
"taxonomy_vocabulary_1":{"und":{"hierarchical_select":{"selects":[1,18,35]}}},
"status":"1",
"field_redtea":{"und":[{"value":"1"}]},
"field_bubbles":{"und":[{"value":"boba poppers","format":null,"safe_value":"boba poppers"}]},
"field_hour_thu_close":{"und":"17"}
}

notes:
1. I've arranged it for legibility and entered only a few random fields to demonstrate how different types of fields are entered. Poster accepted it fine just like this.
2. The hierarchical select is used for the taxonomy; taxonomy_vocabulary_1 is the machine name of my the vocabular that I use here.
3. This is a multilingual site using the i18n module, and I think that is the reason for the "und"
4. The nodes incorporate Location.
5. "status" set to 1 means the post will be published.
6. "field_redtea" is a checkbox field.
7. "field_bubbles" is a multi-line text field.
8. "field_hour_thu_close" is a dropdown selection.