The Tutorial

Note

This tutorial assumes some familiarity with the Falcon framework so make sure you check it out if you haven’t already.

Installation

Using PIP

pip supports installing packages from git repositories:

pip install git+ssh://git@bitbucket.org/leadbrite/req.git@v1.14.0#egg=req

This works inside of requirements files as well:

echo "git+ssh://git@bitbucket.org/leadbrite/req.git@v1.14.0#egg=req" >> requirements.txt

From Source

You can vendor req by copying it into your project. If you go this route make sure that python-dateutil is on your PYTHONPATH.

Goals of the Tutorial

This tutorial will guide you through building a simple blogging application’s API. By the end of this tutorial you will have a sufficient understanding of req to be able to start using it in your own projects.

Structure Your Project

While Falcon and req don’t impose any particular project structure, for the purposes of this tutorial we will structure our project as follows:

example/
├── blog
│   ├── __init__.py
│   ├── endpoints.py   # contains the req Endpoint declarations
│   ├── handlers.py    # contains the Falcon handlers
│   ├── models.py      # contains the database interfaces
│   └── schema.py      # contains the req Schema declarations
└── main.py            # initializes Falcon and registers the Endpoints

Note

Since req does not provide any facilities for interacting with database management systems, we will not focus on the contents of the models.py file. That part is left as an exercise to the reader to implement using their preferred storage engine.

Core Concepts

Before we begin, let’s briefly outline the two major concepts in req:

Schemas
Schemas describe how data is structured. You normally use them to validate user inputs but they’re also usable as containers for data (like models in an ORM) and they can be adapted to various data definition formats like JSONSchema.
Endpoints
Endpoints describe how data can be reached. They expose a domain-specific language for hooking up your routes, schema and handlers.

A Schema for Articles

The API is going to expose endpoints for creating, reading and listing blog Articles, so start by defining how an Article is supposed to be structured:

example/blog/schema.py
1
2
3
4
5
6
from req import schema

class Article(schema.Schema):
    headline = schema.String(_max_length=150)
    content = schema.String()
    author_email = schema.Email(_name="authorEmail")

With that, you get a simple Python interface with which you can construct, render and validate Article data:

>>> from blog.schema import Article

You can construct Articles:

>>> article = Article(
...   headline="Hello, World!",
...   content="...",
...   author_email="bogdan@ave81.com"
... )

Convert them to dictionaries:

>>> article.materialize()
{
  "headline": "Hello, World!",
  "content": "...",
  "authorEmail": "bogdan@ave81.com"
}

Use them to validate inputs:

>>> data = article.materialize()
>>> article.validate(data) is None
True
>>> article.validate({})
Traceback (most recent call last):
  ...
ValidationError: ["headline: missing", "content: missing", "authorEmail: missing"]
>>> article.validate({"authorEmail": "bogdan"})
Traceback (most recent call last):
  ...
ValidationError: ["headline: missing", "content: missing", "authorEmail: invalid e-mail 'bogdan'"]

You can populate their individual properties with data:

>>> article = Article()
>>> article.headline = "Hello"
>>> article.headline
"Hello"
>>> article.content = 42
Traceback (most recent call last):
  ...
ValidationError: invalid string

Finally, you can update all their data at once:

>>> article = Article()
>>> article.update({"headline": "Hello", "content": "...", "authorEmail": "john@example.com"})
>>> article.headline
"Hello"
>>> article.update({"headline": "Hello", "authorEmail": "john@example.com"})
>>> article.content = 42
Traceback (most recent call last):
  ...
ValidationError: ["content: missing"]

Next let’s define a naïve Falcon handler for adding new Articles to the system:

example/blog/handlers.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import json

from falcon import HTTP_201, HTTPBadRequest
from req import ValidationError

from .model import insert_article
from .schema import Article


class Articles(object):
    def on_post(self, request, response):
        try:
            request_bytes = request.stream.read()
            article_dict = json.loads(request_bytes)
            article_schema = Article()
            article_schema.validate(article_schema)
            article_entity = insert_article(article_dict)

            response.status = HTTP_201
            response.body = json.dumps(article_entity)
        except ValueError:
            raise HTTPBadRequest(description="invalid json payload")
        except ValidationError as e:
            raise HTTPBadRequest(description=",".join(e.message))

That’s… not very pretty. In fact, the reason we called that a naïve handler is because req exposes decorators that can help you write the equivalent[1] handler much more concisely:

example/blog/handlers.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from falcon import HTTP_201
from req.falcon.decorators import jsonrequest, jsonresponse

from .models import insert_article


class Articles(object):
    @jsonresponse
    @jsonrequest
    def on_post(self, request, response):
        article = request.context["json"]
        article_entity = insert_article(article)
        return HTTP_201, article_entity

Warning

The order in which the decorators are applied to the on_post method is significant: in order for errors that occur when jsonrequest runs to be formatted correctly, it must be applied to the handler method before jsonresponse. This rule holds for all request/response decorators in req.

Your First Endpoints

Now that you have a working handler, you can declare an endpoint for it:

example/blog/endpoints.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from req import Endpoint, Method

from . import handlers, schema

class V1(Endpoint):
    path = "/blog/v1"

    class Articles(Endpoint):
        # We didn't specify a path so it will default to `/articles`
        handler = handlers.Articles

        class POST(Method):
            Request = schema.Article
            Response = schema.Article

This probably looks a little weird. Let’s look at each chunk in isolation:

class V1(Endpoint):
    path = "/blog/v1"

Declares an Endpoint called V1 whose path is /blog/v1. This will serve as the root of our API and it has no handler or methods of its own.

    class Articles(Endpoint):
        # We didn't specify a path so it will default to `/articles`
        handler = handlers.Articles

Declares an Endpoint called Articles whose path is unspecified, so it will default to /articles. Since this Endpoint is nested under V1 its full path is going to be /blog/v1/articles.

Since its handler points to handlers.Articles, when this Endpoint gets registered with Falcon its route is going to point to an instance of that class.

        class POST(Method):
            Request = schema.Article
            Response = schema.Article

This block states that the Articles endpoint accepts POST requests whose payloads comply with the Article schema and that the response payload will have the same structure. This is how jsonrequest figures out which schema to use when validating requests.

Now that your endpoints are in place, you can register them with falcon:

example/main.py
1
2
3
4
5
6
7
8
9
import blog.endpoints
import falcon
import logging
import req.falcon

logging.basicConfig(level=logging.DEBUG)

app = falcon.API()
req.falcon.register(app, blog.endpoints.V1)

And take your API for a spin:

~/example$ gunicorn main:app
[INFO] Starting gunicorn 19.3.0
[INFO] Listening at: http://127.0.0.1:8000 (20493)
[INFO] Using worker: sync
[INFO] Booting worker with pid: 20496
DEBUG:req.falcon:Registered endpoint 'articles' with path '/blog/v1/articles'...

Req tells us it registered a single endpoint at /blog/v1/articles.

Try creating an article[2]:

$ http POST http://localhost:8000/blog/v1/articles headline="Hello, World!" content="hello" authorEmail="bogdan@ave81.com"
HTTP/1.1 201 Created
Connection: close
Date: Wed, 23 Mar 2016 14:55:20 GMT
Server: gunicorn/19.3.0
content-length: 110
content-type: application/json; charset=utf-8

{
    "_status": {
        "code": 201
    },
    "authorEmail": "bogdan@ave81.com",
    "content": "hello",
    "headline": "Hello, World!"
}

Note

HTTPie[2] automatically generates a JSON payload based on the parameters we give it so the above command is the same as this curl command:

curl -H"Content-type: application/json" \
     -d"{\"headline\": \"Hello, World!\", \"content\": \"hello\", \"authorEmail\": \"bogdan@ave81.com\"}" \
     http://localhost:8000/blog/v1/articles

It looks like that worked; try making some malformed requests:

$ http POST http://localhost:8000/blog/v1/articles
HTTP/1.1 400 Bad Request
Connection: close
Date: Wed, 23 Mar 2016 14:47:05 GMT
Server: gunicorn/19.3.0
content-length: 103
content-type: application/json; charset=utf-8

{
    "_status": {
        "code": 400,
        "errors": [
            {
                "message": "missing Content-Type header",
                "severity": "error"
            }
        ]
    }
}
$ http POST http://localhost:8000/blog/v1/articles content="hello"
HTTP/1.1 400 Bad Request
Connection: close
Date: Wed, 23 Mar 2016 14:49:03 GMT
Server: gunicorn/19.3.0
content-length: 151
content-type: application/json; charset=utf-8

{
    "_status": {
        "code": 400,
        "errors": [
            {
                "message": "headline: missing",
                "severity": "error"
            },
            {
                "message": "authorEmail: missing",
                "severity": "error"
            }
        ]
    }
}
$ http POST http://localhost:8000/blog/v1/articles content="hello" authorEmail="bogdan"
HTTP/1.1 400 Bad Request
Connection: close
Date: Wed, 23 Mar 2016 14:53:19 GMT
Server: gunicorn/19.3.0
content-length: 167
content-type: application/json; charset=utf-8

{
    "_status": {
        "code": 400,
        "errors": [
            {
                "message": "headline: missing",
                "severity": "error"
            },
            {
                "message": "authorEmail: invalid e-mail 'bogdan'",
                "severity": "error"
            }
        ]
    }
}

Looking Up Individual Articles

You’re going to want to be able to grab individual Articles by id after you create them. Unfortunately, Articles don’t contain ids right now. Let’s fix that.

Start by splitting the Article schema into three separate classes:

example/blog/schema.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from req import schema


class BaseArticle(schema.Schema):
    headline = schema.String(_max_length=150)
    content = schema.String()
    author_email = schema.Email(_name="authorEmail")


class CreateArticle(BaseArticle):
    pass


class Article(BaseArticle):
    id = schema.Integer()
BaseArticle
This serves as a representation for the common set of properties present on all Article requests and responses. Other Schemas can extend this and inherit its properties.
CreateArticle
This serves as the Schema for Article creation requests.
Article
This serves as the Schema for Article responses. This has one extra property compared to BaseArticle, the Article’s numeric id.

Update the Articles endpoint to reflect the schema change:

example/blog/endpoints.py
    class Articles(Endpoint):
        handler = handlers.Articles

        class POST(Method):
            Request = schema.CreateArticle
            Response = schema.Article

Now if you create an Article, its response will contain an id property:

$ http POST http://localhost:8000/blog/v1/articles headline="Hello, World!" content="hello" authorEmail="bogdan@ave81.com"
HTTP/1.1 201 Created
Connection: close
Date: Wed, 23 Mar 2016 15:30:01 GMT
Server: gunicorn/19.3.0
content-length: 119
content-type: application/json; charset=utf-8

{
    "_status": {
        "code": 201
    },
    "authorEmail": "bogdan@ave81.com",
    "content": "hello",
    "headline": "Hello, World!",
    "id": 1
}

Note

We’re assuming that the insert_article method was also updated to return each Article’s id.

All you have to do now is add an endpoint and a handler for looking up individual Articles:

example/blog/endpoints.py
    class Articles(Endpoint):
        handler = handlers.Articles

        class Article(Endpoint):
            path = "/{article_id}"
            handler = handlers.Article

            class GET(Method):
                Response = schema.Article

        class POST(Method):
            Request = schema.CreateArticle
            Response = schema.Article

The curly bracket syntax in the new path denotes that we expect the path to contain a dynamic parameter holding the id. That value will be passed into the handler as a keyword argument.

example/blog/handlers.py
from falcon import HTTP_201, HTTPNotFound
from req.falcon.decorators import jsonrequest, jsonresponse

from .models import articles, lookup_article, insert_article

class Article(object):
    @jsonresponse
    def on_get(self, request, response, article_id):
        article = lookup_article(article_id)
        if article is None:
            raise HTTPNotFound(description="article {!r} not found".format(article_id))
        return article

Restart your server and you should see the new endpoint being registered:

~/example$ gunicorn main:app
[INFO] Starting gunicorn 19.3.0
[INFO] Listening at: http://127.0.0.1:8000 (24784)
[INFO] Using worker: sync
[INFO] Booting worker with pid: 24787
DEBUG:req.falcon:Registered endpoint 'articles' with path '/blog/v1/articles'...
DEBUG:req.falcon:Registered endpoint 'article' with path '/blog/v1/articles/{article_id}'...
$ http GET http://localhost:8000/blog/v1/articles/1
HTTP/1.1 404 Not Found
Connection: close
Date: Wed, 23 Mar 2016 15:48:06 GMT
Server: gunicorn/19.3.0
content-length: 97
content-type: application/json; charset=utf-8

{
    "_status": {
        "code": 404,
        "errors": [
            {
                "message": "article '1' not found",
                "severity": "error"
            }
        ]
    }
}

After creating an article:

$ http GET http://localhost:8000/blog/v1/articles/0
HTTP/1.1 200 OK
Connection: close
Date: Wed, 23 Mar 2016 15:53:46 GMT
Server: gunicorn/19.3.0
content-length: 119
content-type: application/json; charset=utf-8

{
    "_status": {
        "code": 200
    },
    "authorEmail": "bogdan@ave81.com",
    "content": "hello",
    "headline": "Hello, World!",
    "id": 0
}

Lists of Articles

Now that you can create Articles and look them up individually, you probably want to list them. Once again, start by defining a schema:

example/blog/schema.py
class Articles(schema.Schema):
    items = schema.List(Article())

Then a handler:

example/blog/handlers.py
class Articles(object):
    @jsonresponse
    def on_get(self, request, response):
        return {"items": articles}

And hook it all up:

example/blog/endpoints.py
    class Articles(Endpoint):
        handler = handlers.Articles

        class Article(Endpoint):
            path = "/{article_id}"
            handler = handlers.Article

            class GET(Method):
                Response = schema.Article

        class GET(Method):
            Response = schema.Articles

        class POST(Method):
            Request = schema.CreateArticle
            Response = schema.Article
$ http GET http://localhost:8000/blog/v1/articles
HTTP/1.1 200 OK
Connection: close
Date: Wed, 23 Mar 2016 16:34:27 GMT
Server: gunicorn/19.3.0
content-length: 132
content-type: application/json; charset=utf-8

{
    "_status": {
        "code": 200
    },
    "items": [
        {
            "authorEmail": "bogdan@ave81.com",
            "content": "hello",
            "headline": "Hello, World!",
            "id": 0
        }
    ]
}

Next Steps

Congratulations on building your first req API! If you’re interested in learning more we suggest you check out the API reference documentation.

Footnotes

[1]The second version of the handler has improved error handling and better validation so it is not exactly “equivalent”.
[2](1, 2) We’re using the httpie utility to make requests to our API because it’s simpler to use than something like curl but you should feel free to use whatever tools you’re familiar with. You might consider using Postman instead of either of those if you prefer GUI tools.