A frequent question from new community members is how to use a single resource class to handle requests for a single item as well as for multiple items. The answer up until now is that the framework doesn't really support this model. The idiomatic way to handle this is as explained in the FAQ.
There have also been attempts to solve this via decorators, as in PR #389. However, it is my belief that this approach requires more typing (is less DRY), and generally makes it more difficult to reason about the entire routing namespace, since the routing definitions are decentralized, being sprinkled about multiple methods in multiple classes and potentially multiple modules. This runs contrary to one of the basic design decisions of the Falcon framework in the way routing is defined vs. other frameworks that prefer object-based or decorator-based routing.
That being said, I understand there is some value in grouping related responders in a single class, and so we should find a way to do so. Below is my proposal for solving the issue without using decorators, while also being straightforward to support in the framework and somewhat easy for the web developer to reason about:
class IssueResource(object):
def on_get(self, req, resp, project_id, issue_id):
# Set response to a representation of the issue
def on_put(self, req, resp, project_id, issue_id):
# Update an existing issue
def on_get_collection(self, req, resp, project_id):
# Return a list of issues for the project
def on_post_collection(self, req, resp, project_id):
# Create a new issue and return its URL
def on_get_shortlink(self, req, resp, shortlink):
# Convert the shortlink to project_id and issue_id,
# then call self.on_get(req, resp, project_id, issue_id)
issue_resource = IssueResource()
api.add_route('/projects/{project_id}/issues/{issue_id}', issue_resource)
api.add_route('/projects/{project_id}/issues', issue_resource, alias='collection')
api.add_route('/s/{shortlink}', issue_resource, alias='short')
What would the alias kwarg do?
So, I'm not entirely sure if the "alias" is the best term, but the idea is that for this route, look for responders that end in the specified (arbitrary) alias, and ignore others. So 'collection' would only recognize on_get_collection and on_post_collection.
Could alias (whatever it ends up being called) get passed as an arg/kwarg to on_get? Then you'd at least get something potentially more helpful than AttributeError when you mess up.
ETA: so you'd have something like
class IssueResource(object):
def on_get(req, resp, project_id, issue_id, alias):
if alias == 'short':
# instead of on_get_short
elif alias == 'collection':
# instead of on_get_collection
else:
# 'normal' on_get
If a developer were to make a typo in a method name, her functional tests would get back a 405, which should be at least marginally helpful in tracking down the problem. But it might be nice to have add_route raise an error in the case that no applicable methods are found on the given resource class.
BTW, another option for the kwarg name is "alt", as in this is an alternate route for the resource. I thought about "specialization" as in the resource representation is specialized for this route, but that doesn't actually make sense in the case of a shortlink which is simply an alias or "alternate" route for the same representation.
api.add_route('/projects/{project_id}/issues', issue_resource, alt='collection')
Is this functionality currently available?
From my point of view, it seems reasonable for a same resource to be able to handle more than one route. For example, I would prefer to handle GET tickets/{num} (retrieve ticket number {num}) and POST tickets (create new ticket) in the same class.
@rvinas It hasn't yet been implemented, but this could be a good project for https://hacktoberfest.digitalocean.com/ if someone is interested in tacking it on for Falcon 1.2.
@kgriffs we have been using a pattern around using a concept of ResourceFactory that takes a resource class which supports both list and detail views and creates two separate resources which is then registered with falcon. I think this solution is quite elegant as it doesn't need any change in falcon. I have extracted this pattern as a package -> https://pypi.python.org/pypi/falcon-resource-factory/
@kgriffs any comments on this?
@kgritesh I think that factory class is a reasonable approach. However, I would still like to build support for this sort of thing directly into the core framework since it is a common request. That being said, the built-in support should be entirely optional and allow for alternative approaches.
@kgriffs I can have a go at this if someone isn't working on it.
@santeyio Nobody has taken this one yet. Would you like to grab it?
@kgriffs yeah I'll give it a shot
add_route() already takes custom *args and **kwargs that could be passed down to the handler.
Considering this and that there are already different methods to add routes, could it be cleaner to add a new method for this purpose? Extending routing API to become something like:
add_route('/resources/{parameter}', resource)add_alternate_route('/resources/{parameter}', resource, 'collection')add_static_route('/static/foo', '/path/to/foo/bar')add_sink(sink)The parameter could, as a suggestion, be called suffix?
I could build if we crystallize the design.
For those wondering how this has been implemented, the source code states:
"""Maps HTTP methods (e.g., GET, POST) to methods of a resource object.
Args:
resource: An object with *responder* methods, following the naming
convention *on_\**, that correspond to each method the resource
supports. For example, if a resource supports GET and POST, it
should define ``on_get(self, req, resp)`` and
``on_post(self, req, resp)``.
Keyword Args:
suffix (str): Optional responder name suffix for this route. If
a suffix is provided, Falcon will map GET requests to
``on_get_{suffix}()``, POST requests to ``on_post_{suffix}()``,
etc.
Returns:
dict: A mapping of HTTP methods to explicitly defined resource responders.
"""
Which basically means that you need to add it into your code this way.
api.add_route('/route/', route_resource, suffix='whatever')
And in your class simply add a responder with this signature:
on_get_whatever(usual stuff)
Awesome work guys, loving falcon!
@mumrau thanks for the explanation I am still trying to understand, how would you send the suffix in the curl for example
@HannaJohn The suffix doesn't have anything to do with the way you query it but with the way you bind it to your code, it links the appropriate responder.
If you re-use the above example:
api.add_route('/route/', route_resource, suffix='whatever') which bind the on_get_whatever responderapi.add_route('/route/{id}', route_resource, suffix='id_query') which would bind to on_get_id_queryapi.add_route('/base/', route_resource) which relates to the on_get, on_post etc...TLDR: the purpose of these suffixes is to _absolutely_ avoid having to change the payload to match the back-end code which would defeat the purpose of a REST api. It just helps (a lot) the codebase as you can regroup similar pieces of code more easily.
I had two routes with two responders respectively
api.add_route('/route', route_resource)
api.add_route('/route', route_resource, suffix='whatever')
and this always led to the base responder.
I managed to use and route to two different responders after changing to
api.add_route('/route', route_resource)
api.add_route('/route/whatever', route_resource, suffix='whatever')
and this what was missing for me in the documentation to understand how to use it
Most helpful comment
For those wondering how this has been implemented, the source code states:
Which basically means that you need to add it into your code this way.
api.add_route('/route/', route_resource, suffix='whatever')And in your class simply add a responder with this signature:
on_get_whatever(usual stuff)Awesome work guys, loving falcon!