Wednesday, October 29, 2014

wheezy web: RESTful API Design

In this article we are going to explore a simple RESTful API created with wheezy.web framework. The demo implements a CRUD for tasks. Includes entity validation, content caching with dependencies and functional test cases. The source code is structured with well defined actors (you can read more about it here).

Design

The following convention is used with respect to operation, HTTP method (verb) and URL:
List:   GET    /api/v1/tasks
Add:    POST   /api/v1/tasks
Get:    GET    /api/v1/task/{task_id}
Update: PUT    /api/v1/task/{task_id}
Remove: DELETE /api/v1/task/{task_id}
The task entity consists of the following attributes: task_id, title and status. The source code is available here. Please download before proceeding any further.

Setup

Create a virtual environment and run application:
virtualenv env
env/bin/pip install wheezy.web
env/bin/python app.py

List Tasks

curl -i http://localhost:8080/api/v1/tasks
HTTP/1.1 200 OK
Date: Wed, 29 Oct 2014 14:14:43 GMT
Server: WSGIServer/0.1 Python/2.7.6
Content-Type: application/json; charset=UTF-8
Cache-Control: private, max-age=0
Expires: Wed, 29 Oct 2014 14:14:43 GMT
Last-Modified: Wed, 29 Oct 2014 14:14:43 GMT
ETag: "d0d2d53e"
Content-Length: 146

{"tasks":[{"status":1,"task_id":"1","title":"Task #1"},
{"status":2,"task_id":"2","title":"Task #2"},
{"status":1,"task_id":"3","title":"Task #3"}]}

Get Task

curl -i http://localhost:8080/api/v1/task/1
HTTP/1.1 200 OK
Date: Wed, 29 Oct 2014 14:17:34 GMT
Server: WSGIServer/0.1 Python/2.7.6
Content-Type: application/json; charset=UTF-8
Cache-Control: private, max-age=0
Expires: Wed, 29 Oct 2014 14:17:34 GMT
Last-Modified: Wed, 29 Oct 2014 14:17:34 GMT
ETag: "a552062f"
Content-Length: 53

{"task":{"status":1,"task_id":"1","title":"Task #1"}}

Add Task

curl -i -H "Content-Type: application/json" -X POST \
  -d '{"title": "Test"}' http://localhost:8080/api/v1/tasks
HTTP/1.1 201 Created
Date: Wed, 29 Oct 2014 14:15:13 GMT
Server: WSGIServer/0.1 Python/2.7.6
Content-Type: application/json; charset=UTF-8
Cache-Control: private
Content-Length: 24

{"task":{"task_id":"4"}}

Update Task

curl -i -H "Content-Type: application/json" -X PUT \
  -d '{"status": 1}' http://localhost:8080/api/v1/task/1
HTTP/1.1 200 OK
Date: Wed, 29 Oct 2014 14:18:46 GMT
Server: WSGIServer/0.1 Python/2.7.6
Content-Type: application/json; charset=UTF-8
Cache-Control: private
Content-Length: 15

{"status":"OK"}

Delete Task

curl -i -X DELETE http://localhost:8080/api/v1/task/4
The output is similar to update task.

Validation Errors

curl -i -H "Content-Type: application/json" -X POST \
  -d '{}' http://localhost:8080/api/v1/tasks
HTTP/1.1 200 OK
Date: Wed, 29 Oct 2014 14:29:50 GMT
Server: WSGIServer/0.1 Python/2.7.6
Content-Type: application/json; charset=UTF-8
Cache-Control: private
Content-Length: 61

{"errors":{"title":["Required field cannot be left blank."]}}

Task Not Found

curl -i http://localhost:8080/api/v1/task/0
HTTP/1.1 404 Not Found
Date: Wed, 29 Oct 2014 14:32:14 GMT
Server: WSGIServer/0.1 Python/2.7.6
Content-Type: text/html; charset=UTF-8
Cache-Control: private, max-age=0
Expires: Wed, 29 Oct 2014 14:32:14 GMT
Last-Modified: Wed, 29 Oct 2014 14:32:14 GMT
ETag: "43be58cc"
Content-Length: 0

HTTP Caching

Ensure a value for If-None-Match corresponds to one returned by list tasks command header ETag.
curl -i -H 'If-None-Match: "d0d2d53e"' \
  http://localhost:8080/api/v1/tasks
HTTP/1.1 304 Not Modified
Date: Wed, 29 Oct 2014 15:13:13 GMT
Server: WSGIServer/0.1 Python/2.7.6
Content-Type: application/json; charset=UTF-8
Cache-Control: private, max-age=0
Expires: Wed, 29 Oct 2014 15:12:39 GMT
Last-Modified: Wed, 29 Oct 2014 15:12:39 GMT
ETag: "d0d2d53e"
Content-Length: 0

Functional Test Cases

The functional test cases can be downloaded from here.
env/bin/pip install pytest
env/bin/py.test test_app.py

7 comments :

  1. Could you briefly explain what the Factory is doing in the source codes? And why do we put data manipulation codes in APIService(ErrorsMixin) and wrap them with "with self.factory('rw') as f", rather than putting them directly in those Handlers?
    I'm a noob to web-dev. Probably these questions sound stupid, but please help! Thanks in advance!

    ReplyDelete
  2. The design of wheezy.web application uses the idea of actors (see here: http://mindref.blogspot.com/2013/09/wheezy-web-actors.html): a separation of concerns, an approach in software architecture that leads to more flexible, predictable and robust software with a use of well known design patterns.

    The factory here plays two roles: a unit of work (db transaction scope, e.g. read only or read write) and factory for business logic which is provided by service bridge pattern, which in turn further is intended to use repository pattern.

    Using handler only may look appropriate at first, however when the application grows you quickly loose clarity which in turn would require refactoring, which sooner or later leads you to design patterns to solve that. So I made one step forward for you.

    ReplyDelete
    Replies
    1. Thanks for your reply! It clarify things a bit for me.

      Delete
    2. Hi Andriy, thanks for you example and Happy New Year!!! Could you please elaborate a bit further on why using handler will lose clarify in the long run? could you please provide some more examples? that would be very helpful! thanks!

      regards.
      Harvey

      Delete
    3. Happy New Year! The clarity (readability) of the source code depends on your ability to quickly understand it (what is where, helicopter view). As the source base grows over the time you need to take a special care to avoid mess. A separation of concerns and use of design patterns in building software help and make it more elegant. In contrast just try to place everything into a handler and later attempt to add audit, caching, sql persistence and/or web service agent support.

      All examples are here: https://bitbucket.org/akorn/wheezy.web/src/tip/demos

      Delete
  3. In the test_app.py there is a call to self.path_for('task_list'). However, I cannot see where the url mapping for 'task_list' is done. App.py has 'tasks' and 'task/{task_id:number}'. Is there a config or other file that I am missing here?

    ReplyDelete
    Replies
    1. The name used by path_for is taken after TaskListHandler class name. See more here:
      http://pythonhosted.org/wheezy.routing/userguide.html#named

      Delete