Sanic Web Tutorial Part 1

Welcome to my Sanic Web Tutorial where you will learn to use the async web framework Sanic to create and deploy your own website.

Assumptions

I assume that you are familiar with using Python and virtualenv, but you do not need to be an expert. You should be able to use this tutorial even if you are new to web development.

I'm developing on a Ubuntu 19.10 machine running Python 3.8, so if you are on a different OS, you might need to make minor changes.

Hello World with Sanic

Start by creating a folder for your new web application and in there create a virtualenv and install Sanic with pip:

$ mkdir sanic_web_tutorial
$ virtualenv venv -p python3
$ source venv/bin/activate
$ pip install sanic

Now we will create our main application file and put some boilerplate Python code in there.

$ touch main.py

Open main.py in an editor and enter this:

from sanic import Sanic
from sanic import response

app = Sanic(name='My Sanic Web App')

@app.route("/")
async def index(request):
    return response.text("Hello World")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

Now, start the web application by entering:

$ python main.py
[2020-02-07 23:35:35 +0800] [6539] [INFO] Goin' Fast @ http://0.0.0.0:8000
[2020-02-07 23:35:35 +0800] [6539] [INFO] Starting worker [6539]

Go to http://0.0.0.0:8000 in your browser to see you web application in action with the text "Hello World" returned.

What goes on in "Hello World"

Lets dig into this tiny Hello World application to understand what exactly goes on.

First, we import two things from the sanic framework:
Sanic: The web application class and
response: which will help us create something that the application can return to the user.

Next, we instantiate the application with app = Sanic(name='My Sanic Web App'). We must provide a 'name' otherwise we will get a warning (or the instantiation will fail).

With @app.route("/") we define a HTTP endpoint, in this case the /. Once our browser hits this endpoint, the function that is defined below it will be executed:

async def index(request):
    return response.text("Hello World")

This defines an async function called 'index'. We could make this example even more simple by removing the 'async' keyword and simply have:

async def index(request):
    return response.text("Hello World")

then our endpoint wouldn't be syncronious though, not async.

The @app.route decorater for the app.add_route method, and instead of using the decorater, we could have just used this method like this:

async def index(request):
    return response.html("<h1>Hello World</h1>")

app.add_route(index, "/")

One advantage of using the add_route method instead of the decorator is that you can add multiple routes to the same function. Say, we want both / and /main to return the same page, we can do this with 2 add_route() calls:

async def index(request):
    return response.html("<h1>Hello World</h1>")

app.add_route(index, "/")
app.add_route(index, "/main")

No automatic server restart on changes
Be mindful, that the running python main.py doesn't restart automatically when you make changes in the code like you might be used to from Flask or Django. You'll have to re-run python main.py yourself.

In the index function we are simply returning a text response with the value "Hello World". If we wanted to return html instead of text, we could have written the function as

@app.route("/")
async def index(request):
    return response.html("<h1>Hello World</h1>")

The only difference between these two response types is the Content-Type, where for text it is text/plain while for the html response it is text/html.
There are other repsonse types available as well, like response.json and response.file, but we will get back to them in time.

Finally, we instantiate the application with

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

This is similar to how Flask works (if you are familiar with Flask). We specify the host and port and with __name__ we can run it with python main.py.

Testing

We could write unittests using Python's unittest module, but we will use pytest (and pytest-sanic) instead as this allows us to write shorter and nicer tests. Additionally, pytest enables more advanced testing options going forward.

First, install pytest-sanic into the virtualenv and create a test file:

$ pip install pytest-sanic
$ touch test.py

In test.py, write a simple test to check that the application returns a 200 on the router /:

from main import app


def test__index__get_request__returns_200():
    request, response = app.test_client.get('/')
    assert response.status == 200

run the test by typing

$ pytest test.py
================================================ test session starts ================================================
platform linux -- Python 3.7.3, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /sanic_web_tutorial
plugins: sanic-1.1.2
collected 1 item                                                                                                    

test.py .                                                                                                     [100%]

================================================= warnings summary ==================================================
test.py::test__index__get_request__returns_200
  /sanic_web_tutorial/venv/lib/python3.7/site-packages/httpx/client.py:234: UserWarning: Passing a 'verify' argument when making a request on a client is due to be deprecated. Instantiate a new client instead, passing any 'verify' arguments to the client itself.
    "Passing a 'verify' argument when making a request on a client "

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=========================================== 1 passed, 1 warning in 0.02s ============================================

Ignore the warning for now. The test passes.

Save

Finally, before closing everything, do a

$ pip freeze > requirements.txt

to create a reusable requirements file. If you are on a Ubuntu system, you might have to manually open the just created requirements.txt file and delete the line pkg-resources==0.0.0. On my system (Ubuntu), this line is wrongly created when running pip freeze (bug in Ubuntu). If you don't delete it, then you will get an error the next time you try and install the requirements with pip install -r requirements.txt.