Intermediate Usage¶
This page covers building a slightly more complex Sanic-RESTful-Api app that will cover out some best practices when setting up a real-world Sanic-RESTful-Api-based API. The Quickstart section is great for getting started with your first Sanic-RESTful-Api app, so if you’re new to Sanic-RESTful-Api you’d be better off checking that out first.
Project Structure¶
There are many different ways to organize your Sanic-RESTful-Api app, but here we’ll describe one that scales pretty well with larger apps and maintains a nice level organization.
The basic idea is to split your app into three main parts: the routes, the resources, and any common infrastructure.
Here’s an example directory structure:
myapi/
__init__.py
app.py # this file contains your app and routes
resources/
__init__.py
foo.py # contains logic for /Foo
bar.py # contains logic for /Bar
common/
__init__.py
util.py # just some common infrastructure
The common directory would probably just contain a set of helper functions to fulfill common needs across your application. It could also contain, for example, any custom input/output types your resources need to get the job done.
In the resource files, you just have your resource objects. So here’s what
foo.py
might look like:
from sanic_restful_api import Resource
class Foo(Resource):
async def get(self):
pass
async def post(self):
pass
The key to this setup lies in app.py
:
from sanic import Sanic
from sanic_restful_api import Api
from myapi.resources.foo import Foo
from myapi.resources.bar import Bar
from myapi.resources.baz import Baz
app = Sanic(__name__)
api = Api(app)
api.add_resource(Foo, '/Foo', '/Foo/<string:id>')
api.add_resource(Bar, '/Bar', '/Bar/<string:id>')
api.add_resource(Baz, '/Baz', '/Baz/<string:id>')
As you can imagine with a particularly large or complex API, this file ends up
being very valuable as a comprehensive list of all the routes and resources in
your API. You would also use this file to set up any config values
(before_request()
, after_request()
).
Basically, this file configures your entire API.
The things in the common directory are just things you’d want to support your resource modules.
Use With Blueprints¶
See blueprints in the Sanic documentation for what blueprints are and
why you should use them. Here’s an example of how to link an Api
up to a Blueprint
.
from sanic import Sanic, Blueprint
from sanic_restful_api import Api, Resource, url_for
app = Sanic(__name__)
api_bp = Blueprint('api', __name__)
api = Api(api_bp)
class TodoItem(Resource):
async def get(self, id):
return {'task': 'Say "Hello, World!"'}
api.add_resource(TodoItem, '/todos/<int:id>')
app.register_blueprint(api_bp)
Note
Calling Api.init_app()
is not required here because registering the
blueprint with the app takes care of setting up the routing for the
application.
Full Parameter Parsing Example¶
Elsewhere in the documentation, we’ve described how to use the reqparse example in detail. Here we’ll set up a resource with multiple input parameters that exercise a larger amount of options. We’ll define a resource named “User”.
from sanic_restful_api import fields, marshal_with, reqparse, Resource
def email(email_str):
"""Return email_str if valid, raise an exception in other case."""
if valid_email(email_str):
return email_str
else:
raise ValueError('{} is not a valid email'.format(email_str))
post_parser = reqparse.RequestParser()
post_parser.add_argument(
'username', dest='username',
location='form', required=True,
help='The user\'s username',
)
post_parser.add_argument(
'email', dest='email',
type=email, location='form',
required=True, help='The user\'s email',
)
post_parser.add_argument(
'user_priority', dest='user_priority',
type=int, location='form',
default=1, choices=range(5), help='The user\'s priority',
)
user_fields = {
'id': fields.Integer,
'username': fields.String,
'email': fields.String,
'user_priority': fields.Integer,
'custom_greeting': fields.FormattedString('Hey there {username}!'),
'date_created': fields.DateTime,
'date_updated': fields.DateTime,
'links': fields.Nested({
'friends': fields.Url('user_friends'),
'posts': fields.Url('user_posts'),
}),
}
class User(Resource):
@marshal_with(user_fields)
async def post(self):
args = post_parser.parse_args()
user = create_user(args.username, args.email, args.user_priority)
return user
@marshal_with(user_fields)
async def get(self, id):
args = post_parser.parse_args()
user = fetch_user(id)
return user
As you can see, we create a post_parser
specifically to handle the parsing
of arguments provided on POST. Let’s step through the definition of each
argument.
post_parser.add_argument(
'username', dest='username',
location='form', required=True,
help='The user\'s username',
)
The username
field is the most normal out of all of them. It takes
a string from the POST body and converts it to a string type. This argument
is required (required=True
), which means that if it isn’t provided,
Sanic-RESTful-Api will automatically return a 400 with a message along the lines
of ‘the username field is required’.
post_parser.add_argument(
'email', dest='email',
type=email, location='form',
required=True, help='The user\'s email',
)
The email
field has a custom type of email
. A few lines earlier we
defined an email
function that takes a string and returns it if the type is
valid, else it raises an exception, exclaiming that the email type was
invalid.
post_parser.add_argument(
'user_priority', dest='user_priority',
type=int, location='form',
default=1, choices=range(5), help='The user\'s priority',
)
The user_priority
type takes advantage of the choices
argument. This
means that if the provided user_priority value doesn’t fall in the range
specified by the choices
argument (in this case [0, 1, 2, 3, 4]
),
Sanic-RESTful-Api will automatically respond with a 400 and a descriptive error
message.
That covers the inputs. We also defined some interesting field types in the
user_fields
dictionary to showcase a couple of the more exotic types.
user_fields = {
'id': fields.Integer,
'username': fields.String,
'email': fields.String,
'user_priority': fields.Integer,
'custom_greeting': fields.FormattedString('Hey there {username}!'),
'date_created': fields.DateTime,
'date_updated': fields.DateTime,
'links': fields.Nested({
'friends': fields.Url('user_friends', absolute=True),
'posts': fields.Url('user_friends', absolute=True),
}),
}
First up, there’s fields.FormattedString
.
'custom_greeting': fields.FormattedString('Hey there {username}!'),
This field is primarily used to interpolate values from the response into
other values. In this instance, custom_greeting
will always contain the
value returned from the username
field.
Next up, check out fields.Nested
.
'links': fields.Nested({
'friends': fields.Url('user_friends', absolute=True),
'posts': fields.Url('user_posts', absolute=True),
}),
This field is used to create a sub-object in the response. In this case,
we want to create a links
sub-object to contain urls of related objects.
Note that we passed fields.Nested another dict which is built in such a
way that it would be an acceptable argument to marshal()
by itself.
Finally, we used the fields.Url
field type.
'friends': fields.Url('user_friends', absolute=True),
'posts': fields.Url('user_friends', absolute=True),
It takes as its first parameter the name of the endpoint associated with the
urls of the objects in the links
sub-object. Passing absolute=True
ensures that the generated urls will have the hostname included.
Passing Constructor Parameters Into Resources¶
Your Resource
implementation may require outside dependencies. Those
dependencies are best passed-in through the constructor to loosely couple each
other. The Api.add_resource()
method has two keyword arguments:
resource_class_args
and resource_class_kwargs
. Their values will be forwarded
and passed into your Resource implementation’s constructor.
So you could have a Resource
:
from sanic_restful_api import Resource
class TodoNext(Resource):
def __init__(self, **kwargs):
# smart_engine is a black box dependency
self.smart_engine = kwargs['smart_engine']
async def get(self):
return self.smart_engine.next_todo()
You can inject the required dependency into TodoNext like so:
smart_engine = SmartEngine()
api.add_resource(TodoNext, '/next',
resource_class_kwargs={ 'smart_engine': smart_engine })
Same idea applies for forwarding args.