ROA (Resource Oriented Architecture)

Resource Oriented Architecture (REST) is incredible popular nowadays for the following reasons:

  • Increased scalability of applications.
  • Easy integration of systems.
  • Intuitive modelling of business problems.
  • Stateful imperative programming pains removed.

You can find many information about advantages of REST and why it is recommended to use such an architecure. For further reading you can visit http://en.wikipedia.org/wiki/Representational_state_transfer.

In Fantastisco framework we firmly encourage REST approach into projects. We even go a step further in this direction, by standardising REST APIs and providing REST APIs generator over implemented models.

Examples

Application settings stored in database

Imagine you have a model called AppSetting meant to define custom settings attributes which influence your application.

class AppSetting(BASEMODEL):
    __tablename__ = "app_settings"

    id = Column("id", Integer, primary_key=True, autoincrement=True)
    name = Column("name", String(50), unique=True, nullable=False)
    value = Column("value", Text, nullable=False)

    def __init__(self, name, value):
        self.name = name
        self.value = value

In a standard MVC (Model View Controller) web application once you have defined the above mentioned fantastico model you would have to do the following: (for providing minimal CRUD)

  1. Create a fantastico.mvc.controller_decorators.Controller
  2. Implement listing of custom settings
    1. Support pagination.
    2. Support filtering.
    3. Support ordering.
  3. Implement individual custom setting retrieval (by id).
  4. Implement Create custom setting.
  5. Implement Update custom setting.
  6. Implement Delete custom setting.
  7. For each operation implemented provide validation logic.
  8. For each operation implemented provide error handling logic.

This is an extremely repetitive task and involves quite a lot of boiler plate. In addition no standard is imposed for how pagination, sorting and filtering work.

A more convenient way for this problem is to provide some additional information about the model:

@Resource(name="app-setting", url="/app-settings")
class AppSetting(BASEMODEL):
    __tablename__ = "app_settings"

    id = Column("id", Integer, primary_key=True, autoincrement=True)
    name = Column("name", String(50), unique=True, nullable=False)
    value = Column("value", Text, nullable=False)

    def __init__(self, name, value):
        self.name = name
        self.value = value

Once the model is decorated, I expect to have a fully functional API which I can easily invoke through HTTP calls:

  • GET - /api/latest/app-settings - list all application settings (supports filtering, ordering and pagination)
  • POST - /api/latest/app-settings - create a new app setting.
  • PUT - /api/latest/app-settings/:id - update an existing application setting.
  • DELETE - /api/latest/app-settings/:id - delete an existing application setting.

Versioning

It is always a good practice to support API versioning. Going a step further with AppSetting resource:

@Resource(name="app-setting", url="/app-settings", version=1.0)
class AppSetting(BASEMODEL):
    __tablename__ = "app_settings"

    id = Column("id", Integer, primary_key=True, autoincrement=True)
    name = Column("name", String(50), unique=True, nullable=False)
    value = Column("value", Text, nullable=False)

    def __init__(self, name, value):
        self.name = name
        self.value = value

@Resource(name="app-setting", url="/app-settings", version=2.0)
class AppSettingV2(BASEMODEL):
    __tablename__ = "app_settings"

    id = Column("id", Integer, primary_key=True, autoincrement=True)
    name = Column("name", String(80), unique=True, nullable=False)
    value = Column("value", Text, nullable=False)

    def __init__(self, name, value):
        self.name = name
        self.value = value

The above example will actually provide the following endpoints which can be easily accessible:

  • /api/1.0/app-settings
  • /api/2.0/app-settings
  • /api/latest/app-settings (which at this moment points to the most recent version of the api)

If we want to retrieve all application settings using version 1.0 we open a browser and point it to /api/1.0/app-settings. For avoiding multiple APIs chaos we strongly encourage to use the latest available API.

Validation

Each resource requires validation for create / update operations. Validation is harder to be achieved through code introspection so in Fantastico for each defined resource you can define a validator which will be invoked automatically.

class AppSettingValidator(ResourceValidator):
    def validate(self, resource, request, existing_resource_id=None):
        errors = []

        if resource.name == "unsupported":
            errors.append("Invalid setting name: %s" % resource.name)

        if len(resource.value) == 0:
            errors.append("Setting %s value can not be empty. %s" % resource.name)

        if len(errors) == 0:
            return

        raise FantasticoRoaError("\n".join(errors))
@Resource(name="app-setting", url="/app-settings", version=2.0, validator=AppSettingValidator)
class AppSettingV2(BASEMODEL):
    id = Column("id", Integer, primary_key=True, autoincrement=True)
    name = Column("name", String(80), unique=True, nullable=False)
    value = Column("value", Text, nullable=False)

    def __init__(self, name, value):
        self.name = name
        self.value = value

If no validator is provided no validation is done on the given resource. Also, it is important to always remember that validators are only invoked for Create and Update:

  • POST - /api/latest/app-settings - create a new app setting. (validate method will be invoked).
  • PUT - /api/latest/app-settings/:id - update an existing app setting. (validate method will be invoked).

We are aware that there are some common validation cases which can be reused:

  1. Email validation
  2. Phone number validation
  3. Credit Card number validation

All common validation cases are provided out of the box as methods part of ResourceValidator class. You can easily use them into your resource validator.

There are some rare cases when a resource contains sensitive data (e.g user passwords / credit card numbers). In order to alter or suppress such sensitive data you can simply override format_resource method. Below you can find a simple example of user data retrieval where password is never sent back to client even if requested:

class UserValidator(ResourceValidator):
   def format_resource(self, user, request):
      user.password = None

With the above example, whenever you request GET on /users or GET on /user/:userid password will be suppressed.

Partial object representation

There are cases when a resource contains many fields but you actually need only a few of them:

@Resource(name="address", url="/addresses", version=1.0)
class Address(BASEMODEL):
    id = Column("id", Integer, primary_key=True, autoincrement=True)
    line1 = Column("line1", String(200), nullable=False)
    line2 = Column("line2", String(200))
    line3 = Column("line3", String(200))
    line4 = Column("line4", String(200))
    line5 = Column("line5", String(200))
    line6 = Column("line6", String(200))
    city = Column("city", String(80))
    country = Column("country", String(80))
    zip_code = Column("zip_code", String(10))

    def __init__(self, line1=None, line2=None, line3=None, line4=None, line5=None, line6=None,
               city=None, country=None, zip_code=None):
        self.line1 = line1
        self.line2 = line2
        self.line3 = line3
        self.line4 = line4
        self.line5 = line5
        self.line6 = line6
        self.city = city
        self.country = country
        self.zip_code = zip_code

When working with the Address resource there will be cases when we do not need all fields to be transferred to client. For this, partial representation is supported out of the box into Fantastico:

// retrieve only city,zip_code and line1 of a given address
var url = "/api/1.0/addresses/1?fields=city,zip_code,line1";

A possible response for this request might be:

{
    city: "Bucharest",
    zip_code: "B00001",
    line1: "First line of this wonderful address"
}

fields Query parameter is optional. If you omit this query parameter all fields are retrieved in the response. fields query parameter makes sense for:

  1. Listing a collection
  2. Retrieving information about an individual item.

All other operations simply ignore fields.

Resource composed attributes

There are resources which have attributes which points to another resource:

@Resource(name="person", url="/persons", version=1.0,
          subresources={"bill_address": ["bill_address_id"],
                        "mail_address": ["mail_address_id"],
                        "ship_address:" ["ship_address_id"]})
class Person(BASEMODEL):
    __tablename__ = "persons"

    id = Column("id", Integer, primary_key=True, autoincrement=True)
    first_name = Column("first_name", String(80))
    last_name = Column("last_name", String(50))
    bill_address_id = Column("bill_address_id", ForeignKey("addresses.id"))
    bill_address = relationship(Address, primaryjoin=bill_address_id == Address.id)
    ship_address_id = Column("ship_address_id", ForeignKey("addresses.id"))
    ship_address = relationship(Address, primaryjoin=ship_address_id == Address.id)
    mail_address_id = Column("ship_address_id", ForeignKey("addresses.id"))
    ship_address = relationship(Address, primaryjoin=mail_address_id == Address.id)

The above definition shows you how to mark the subresources of person resource. By default they will not be retrieved in requests. Only subresource identifier keys pointing from person to various address objects are retrieved. If you want to obtain details about a specific address (e.g bill_address) you can use example below:

var url = "/api/1.0/persons/1?fields=first_name, last_name, bill_address(line1, city, zip_code)"

The above example url might return:

{
   "first_name": "John",
   "last_name": "Doe",
   "bill_address": {
      "line1": "First line of this wonderful address",
      "city": "Bucharest",
      "zip_code": "B00001"
   }
}

Composed attributes usage is limited to below mentioned operations:

  • Listing collections.
  • Retrieving information about an individual item.
  • First level subresources.

We do not support update / create of multiple resources using one single request.

Security

Fantastico provides a compliant OAuth 2 RFC implementation which is also integrated with ROA. For more information about enabling OAuth 2 authorization on ROA please read Controllers security.