Content Manipulation

plone.restapi does not only expose content objects via a RESTful API. The API consumer can create, read, update, and delete a content object. Those operations can be mapped to the HTTP verbs POST (Create), GET (Read), PUT (Update) and DELETE (Delete).

Manipulating resources across the network by using HTTP as an application protocol is one of core principles of the REST architectural pattern. This allows us to interact with a specific resource in a standardized way:

Verb URL Action
POST /folder Creates a new document within the folder
GET /folder/{document-id} Request the current state of the document
PATCH /folder/{document-id} Update the document details
DELETE /folder/{document-id} Remove the document

Creating a Resource with POST

To create a new resource, we send a POST request to the resource container. If we want to create a new document within an existing folder, we send a POST request to that folder:

http

POST /plone/folder HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "@type": "Document",
    "title": "My Document"
}

curl

curl -i -X POST http://nohost/plone/folder -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"@type": "Document", "title": "My Document"}' --user admin:secret

httpie

echo '{
  "@type": "Document",
  "title": "My Document"
}' | http POST http://nohost/plone/folder Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.post('http://nohost/plone/folder', headers={
    'Accept': 'application/json',
    'Content-Type': 'application/json',
}, json={
    '@type': 'Document',
    'title': 'My Document',
}, auth=('admin', 'secret'))

By setting the ‘Accept’ header, we tell the server that we would like to receive the response in the ‘application/json’ representation format.

The ‘Content-Type’ header indicates that the body uses the ‘application/json’ format.

The request body contains the minimal necessary information needed to create a document (the type and the title). You could set other properties, like “description” here as well.

Successful Response (201 Created)

If a resource has been created, the server responds with the 201 Created status code. The ‘Location’ header contains the URL of the newly created resource and the resource representation in the payload:

HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/folder/my-document

{
  "@components": {
    "actions": {
      "@id": "http://localhost:55001/plone/folder/my-document/@actions"
    }, 
    "breadcrumbs": {
      "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
    }, 
    "navigation": {
      "@id": "http://localhost:55001/plone/folder/my-document/@navigation"
    }, 
    "types": {
      "@id": "http://localhost:55001/plone/folder/my-document/@types"
    }, 
    "workflow": {
      "@id": "http://localhost:55001/plone/folder/my-document/@workflow"
    }
  }, 
  "@id": "http://localhost:55001/plone/folder/my-document", 
  "@type": "Document", 
  "UID": "SomeUUID000000000000000000000005", 
  "allow_discussion": false, 
  "changeNote": "", 
  "contributors": [], 
  "created": "1995-07-31T13:45:00", 
  "creators": [
    "admin"
  ], 
  "description": "", 
  "effective": null, 
  "exclude_from_nav": false, 
  "expires": null, 
  "id": "my-document", 
  "is_folderish": false, 
  "language": "", 
  "layout": "document_view", 
  "modified": "1995-07-31T17:30:00", 
  "parent": {
    "@id": "http://localhost:55001/plone/folder", 
    "@type": "Folder", 
    "description": "This is a folder with two documents", 
    "review_state": "private", 
    "title": "My Folder"
  }, 
  "relatedItems": [], 
  "review_state": "private", 
  "rights": "", 
  "subjects": [], 
  "table_of_contents": null, 
  "text": null, 
  "title": "My Document", 
  "version": "current", 
  "versioning_enabled": true
}

Unsuccessful Response (400 Bad Request)

If the resource could not be created, for instance because the title was missing in the request, the server responds with 400 Bad Request:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  'message': 'Required title field is missing'
}

The response body can contain information about why the request failed.

Unsuccessful Response (500 Internal Server Error)

If the server can not properly process a request, it responds with 500 Internal Server Error:

HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  'message': 'Internal Server Error'
}

The response body can contain further information such as an error trace or a link to the documentation.

Possible POST Responses

Possible server reponses for a POST request are:

POST Implementation

A pseudo-code example of the POST implementation on the server:

try:
    order = createOrder()
    if order == None:
        # Bad Request
        response.setStatus(400)
    else:
        # Created
        response.setStatus(201)
except:
    # Internal Server Error
    response.setStatus(500)

TODO: Link to the real implementation… [

Reading a Resource with GET

After a successful POST, we can access the resource by sending a GET request to the resource URL:

http

GET /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i http://nohost/plone/folder/my-document -H 'Accept: application/json' --user admin:secret

httpie

http http://nohost/plone/folder/my-document Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/folder/my-document', headers={
    'Accept': 'application/json',
}, auth=('admin', 'secret'))

Successful Response (200 OK)

If a resource has been retrieved successfully, the server responds with 200 OK:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "@components": {
    "actions": {
      "@id": "http://localhost:55001/plone/folder/my-document/@actions"
    }, 
    "breadcrumbs": {
      "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
    }, 
    "navigation": {
      "@id": "http://localhost:55001/plone/folder/my-document/@navigation"
    }, 
    "types": {
      "@id": "http://localhost:55001/plone/folder/my-document/@types"
    }, 
    "workflow": {
      "@id": "http://localhost:55001/plone/folder/my-document/@workflow"
    }
  }, 
  "@id": "http://localhost:55001/plone/folder/my-document", 
  "@type": "Document", 
  "UID": "SomeUUID000000000000000000000005", 
  "allow_discussion": false, 
  "changeNote": "", 
  "contributors": [], 
  "created": "1995-07-31T13:45:00", 
  "creators": [
    "admin"
  ], 
  "description": "", 
  "effective": null, 
  "exclude_from_nav": false, 
  "expires": null, 
  "id": "my-document", 
  "is_folderish": false, 
  "language": "", 
  "layout": "document_view", 
  "modified": "1995-07-31T17:30:00", 
  "parent": {
    "@id": "http://localhost:55001/plone/folder", 
    "@type": "Folder", 
    "description": "This is a folder with two documents", 
    "review_state": "private", 
    "title": "My Folder"
  }, 
  "relatedItems": [], 
  "review_state": "private", 
  "rights": "", 
  "subjects": [], 
  "table_of_contents": null, 
  "text": null, 
  "title": "My Document", 
  "version": "current", 
  "versioning_enabled": true
}

For folderish types, their childrens are automatically included in the response as items. To disable the inclusion, add the GET parameter include_items=false to the URL.

By default only basic metadata is included. To include additional metadata, you can specify the names of the properties with the metadata_fields parameter. See also Retrieving additional metadata.

The following example additionaly retrieves the UID and Creator:

http

GET /plone/folder?metadata_fields=UID&metadata_fields=Creator HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i 'http://nohost/plone/folder?metadata_fields=UID&metadata_fields=Creator' -H 'Accept: application/json' --user admin:secret

httpie

http 'http://nohost/plone/folder?metadata_fields=UID&metadata_fields=Creator' Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/folder?metadata_fields=UID&metadata_fields=Creator', headers={
    'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
  "@components": {
    "actions": {
      "@id": "http://localhost:55001/plone/folder/@actions"
    }, 
    "breadcrumbs": {
      "@id": "http://localhost:55001/plone/folder/@breadcrumbs"
    }, 
    "navigation": {
      "@id": "http://localhost:55001/plone/folder/@navigation"
    }, 
    "types": {
      "@id": "http://localhost:55001/plone/folder/@types"
    }, 
    "workflow": {
      "@id": "http://localhost:55001/plone/folder/@workflow"
    }
  }, 
  "@id": "http://localhost:55001/plone/folder?metadata_fields=UID&metadata_fields=Creator", 
  "@type": "Folder", 
  "UID": "SomeUUID000000000000000000000002", 
  "allow_discussion": false, 
  "contributors": [], 
  "created": "1995-07-31T13:45:00", 
  "creators": [
    "test_user_1_"
  ], 
  "description": "This is a folder with two documents", 
  "effective": null, 
  "exclude_from_nav": false, 
  "expires": null, 
  "id": "folder", 
  "is_folderish": true, 
  "items": [
    {
      "@id": "http://localhost:55001/plone/folder/doc1", 
      "@type": "Document", 
      "Creator": "test_user_1_", 
      "UID": "SomeUUID000000000000000000000003", 
      "description": "", 
      "review_state": "private", 
      "title": "A document within a folder"
    }, 
    {
      "@id": "http://localhost:55001/plone/folder/doc2", 
      "@type": "Document", 
      "Creator": "test_user_1_", 
      "UID": "SomeUUID000000000000000000000004", 
      "description": "", 
      "review_state": "private", 
      "title": "A document within a folder"
    }, 
    {
      "@id": "http://localhost:55001/plone/folder/my-document", 
      "@type": "Document", 
      "Creator": "admin", 
      "UID": "SomeUUID000000000000000000000005", 
      "description": "", 
      "review_state": "private", 
      "title": "My Document"
    }
  ], 
  "items_total": 3, 
  "language": "", 
  "layout": "listing_view", 
  "modified": "1995-07-31T17:30:00", 
  "nextPreviousEnabled": false, 
  "parent": {
    "@id": "http://localhost:55001/plone", 
    "@type": "Plone Site", 
    "description": "", 
    "title": "Plone site"
  }, 
  "relatedItems": [], 
  "review_state": "private", 
  "rights": "", 
  "subjects": [], 
  "title": "My Folder", 
  "version": "current"
}

Note

For folderish types, collections or search results, the results will be batched if the size of the resultset exceeds the batch size. See Batching for more details on how to work with batched results.

Unsuccessful response (404 Not Found)

If a resource could not be found, the server will respond with 404 Not Found:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  'error': 'NotFound'
}

GET Implementation

A pseudo-code example of the GET implementation on the server:

try:
    order = getOrder()
    if order == None:
        # Not Found
        response.setStatus(404)
    else:
        # OK
        response.setStatus(200)
except:
    # Internal Server Error
    response.setStatus(500)

You can find implementation details in the plone.restapi.services.content.add.FolderPost class

GET Responses

Possible server reponses for a GET request are:

Updating a Resource with PATCH

To update an existing resource we send a PATCH request to the server. PATCH allows to provide just a subset of the resource (the values you actually want to change).

If you send the value null for a field, the field’s content will be deleted and the missing_value defined for the field in the schema will be set. Note that this is not possible if the field is required, and it only works for Dexterity types, not Archetypes:

http

PATCH /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "title": "My New Document Title"
}

curl

curl -i -X PATCH http://nohost/plone/folder/my-document -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"title": "My New Document Title"}' --user admin:secret

httpie

echo '{
  "title": "My New Document Title"
}' | http PATCH http://nohost/plone/folder/my-document Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.patch('http://nohost/plone/folder/my-document', headers={
    'Accept': 'application/json',
    'Content-Type': 'application/json',
}, json={
    'title': 'My New Document Title',
}, auth=('admin', 'secret'))

Successful Response (204 No Content)

A successful response to a PATCH request will be indicated by a 204 No Content response by default:

HTTP/1.1 204 No Content

Successful Response (200 OK)

You can get the object representation by adding a Prefer header with a value of return=representation to the PATCH request. In this case, the response will be a 200 OK:

http

PATCH /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Prefer: return=representation
Content-Type: application/json

{
    "title": "My New Document Title"
}

curl

curl -i -X PATCH http://nohost/plone/folder/my-document -H 'Accept: application/json' -H 'Content-Type: application/json' -H 'Prefer: return=representation' --data-raw '{"title": "My New Document Title"}' --user admin:secret

httpie

echo '{
  "title": "My New Document Title"
}' | http PATCH http://nohost/plone/folder/my-document Accept:application/json Content-Type:application/json Prefer:return=representation -a admin:secret

python-requests

requests.patch('http://nohost/plone/folder/my-document', headers={
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Prefer': 'return=representation',
}, json={
    'title': 'My New Document Title',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
  "@components": {
    "actions": {
      "@id": "http://localhost:55001/plone/folder/my-document/@actions"
    }, 
    "breadcrumbs": {
      "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
    }, 
    "navigation": {
      "@id": "http://localhost:55001/plone/folder/my-document/@navigation"
    }, 
    "types": {
      "@id": "http://localhost:55001/plone/folder/my-document/@types"
    }, 
    "workflow": {
      "@id": "http://localhost:55001/plone/folder/my-document/@workflow"
    }
  }, 
  "@id": "http://localhost:55001/plone/folder/my-document", 
  "@type": "Document", 
  "UID": "SomeUUID000000000000000000000005", 
  "allow_discussion": false, 
  "changeNote": "", 
  "contributors": [], 
  "created": "1995-07-31T13:45:00", 
  "creators": [
    "admin"
  ], 
  "description": "", 
  "effective": null, 
  "exclude_from_nav": false, 
  "expires": null, 
  "id": "my-document", 
  "is_folderish": false, 
  "language": "", 
  "layout": "document_view", 
  "modified": "1995-07-31T17:30:00", 
  "parent": {
    "@id": "http://localhost:55001/plone/folder", 
    "@type": "Folder", 
    "description": "This is a folder with two documents", 
    "review_state": "private", 
    "title": "My Folder"
  }, 
  "relatedItems": [], 
  "review_state": "private", 
  "rights": "", 
  "subjects": [], 
  "table_of_contents": null, 
  "text": null, 
  "title": "My New Document Title", 
  "version": "current", 
  "versioning_enabled": true
}

See for full specs the RFC 5789: PATCH Method for HTTP

Replacing a Resource with PUT

Note

PUT is not implemented yet.

To replace an existing resource we send a PUT request to the server:

TODO: Add example.

In accordance with the HTTP specification, a successful PUT will not create a new resource or produce a new URL.

PUT expects the entire resource representation to be supplied to the server, rather than just changes to the resource state. This is usually not a problem since the consumer application requested the resource representation before a PUT anyways.

When the PUT request is accepted and processed by the service, the consumer will receive a 204 No Content response (200 OK would be a valid alternative).

Successful Update (204 No Content)

When a resource has been updated successfully, the server sends a 204 No Content response:

TODO: Add example.

Unsuccessful Update (409 Conflict)

Sometimes requests fail due to incompatible changes. The response body includes additional information about the problem.

TODO: Add example.

PUT Implementation

A pseudo-code example of the PUT implementation on the server:

try:
    order = getOrder()
    if order:
        try:
            saveOrder()
        except conflict:
            response.setStatus(409)
        # OK
        response.setStatus(200)
    else:
        # Not Found
        response.setStatus(404)
except:
    # Internal Server Error
    response.setStatus(500)

TODO: Link to the real implementation…

PUT Responses

Possible server reponses for a PUT request are:

POST vs. PUT

Difference between POST and PUT:

  • Use POST to create a resource identified by a service-generated URI
  • Use POST to append a resource to a collection identified by a service-generated URI
  • Use PUT to overwrite a resource

This follows RFC 7231: HTTP 1.1: PUT Method.

Removing a Resource with DELETE

We can delete an existing resource by sending a DELETE request:

http

DELETE /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X DELETE http://nohost/plone/folder/my-document -H 'Accept: application/json' --user admin:secret

httpie

http DELETE http://nohost/plone/folder/my-document Accept:application/json -a admin:secret

python-requests

requests.delete('http://nohost/plone/folder/my-document', headers={
    'Accept': 'application/json',
}, auth=('admin', 'secret'))

A successful response will be indicated by a 204 No Content response:

HTTP/1.1 204 No Content

DELETE Implementation

A pseudo-code example of the DELETE implementation on the server:

try:
    order = getOrder()
    if order:
        if can_delete(order):
            # No Content
            response.setStatus(204)
        else:
            # Not Allowed
            response.setStatus(405)
    else:
        # Not Found
        response.setStatus(404)
except:
    # Internal Server Error
    response.setStatus(500)

TODO: Link to the real implementation…

DELETE Responses

Possible responses to a delete request are:

Reordering sub resources

The resources contained within a resource can be reordered using the ordering key using a PATCH request on the container.

Use the obj_id subkey to specify which resource to reorder. The subkey delta can be ‘top’, ‘bottom’, or a negative or positive integer for moving up or down.

Reordering resources within a subset of resources can be done using the subset_ids subkey.

A response 400 BadRequest with a message ‘Client/server ordering mismatch’ will be returned if the value is not in the same order as serverside.

A response 400 BadRequest with a message ‘Content ordering is not supported by this resource’ will be returned if the container does not support ordering.

http

PATCH /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json

{
    "ordering": {"obj_id": "item_3", "delta": "top", "subset_ids": ["item_1", "item_3", "item5"]}
}

curl

curl -i -X PATCH http://nohost/plone/folder/my-document -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"ordering": {"delta": "top", "obj_id": "item_3", "subset_ids": ["item_1", "item_3", "item5"]}}' --user admin:secret

httpie

echo '{
  "ordering": {
    "delta": "top",
    "obj_id": "item_3",
    "subset_ids": [
      "item_1",
      "item_3",
      "item5"
    ]
  }
}' | http PATCH http://nohost/plone/folder/my-document Accept:application/json Content-Type:application/json -a admin:secret

python-requests

requests.patch('http://nohost/plone/folder/my-document', headers={
    'Accept': 'application/json',
    'Content-Type': 'application/json',
}, json={
    'ordering': {
        'delta': 'top',
        'obj_id': 'item_3',
        'subset_ids': ['item_1', 'item_3', 'item5'],
    },
}, auth=('admin', 'secret'))