How to build a simple HTTP API
Anvil isn’t just the easiest way to build Web user interfaces in Python. It’s also a fully-featured app platform, great for building serverless apps and HTTP APIs!
It’s pretty simple. Just create an Anvil app, add a Server Module, and write an @anvil.server.http_endpoint()
function:
@anvil.server.http_endpoint('/hello-world')
def hello_world(**q):
return {'the answer is': 42}
Note: This guide includes screenshots of the Classic Editor. Since we created this guide, we've released the new Anvil Editor, which is more powerful and easier to use.
All the code in this guide will work, but the Anvil Editor will look a little different to the screenshots you see here!
That was pretty fast, right? And you didn’t have to host it anywhere: Once you publish your app, your API is live!
What does an API look like?
That depends on your app, of course! But for this post, we’ll be building an API for our multi-user To-Do List example.
Our To-Do list is a classic data-storage app. An API for this kind of app needs to do four things:
- Read records - in this case, we want to make a
GET
request to a URL and get all the tasks in our to-do list. - Create new records - in this case, we want to
POST
to a URL to add a new task. - Update records - in this case, we want to mark tasks as done, or change their title. By convention, each task has its own URL, which we can update by making a
PUT
request to it. - Delete records - in this case, by making a
DELETE
request to a task’s URL.
(This is often abbreviated “CRUD”, for Create/Read/Update/Delete.)
Let’s get going!
As we build up this API, step by step, we’ll see common patterns used by many HTTP or REST APIs:
-
Returning records from your database as JSON.
-
Authentication – Make sure only authorised users can access your API.
-
Accepting Parameters – To create new records, we’ll have to take parameters with a POST request.
-
URL parameters let us make custom URLs for each record.
-
Updates and Deletes finish up our API by handing
PUT
andDELETE
requests. -
Further Reading – Learn about other response parameters, CORS made simple, XSRF protection, and more.
If you’re feeling impatient, you can get the source code for the finished, working API:
Returning records from your database
We’ll start with the simplest endpoint: getting all the tasks in our to-do list, as an array of JSON objects. It’s pretty simple – if we return a list of dictionaries, Anvil will turn it into JSON for us:
@anvil.server.http_endpoint('/tasks')
def get_tasks(**q):
return [{'title': task['title'], 'done': task['done']}
for task in app_tables.tasks.search()]
If we published our app at https://my-todo-list.anvil.app
, we can run this function by fetching https://my-todo-list.anvil.app/_/api/tasks
:
$ curl https://my-todo-list.anvil.app/_/api/tasks
[{"done":true,"title":"Wash the car"},{"done":true,"title":"Do the dishes"}]
Note: If you haven’t made your app public, your API URLs will be long and difficult to remember. To get a nicer URL, make your app public from the Publish dialog. See: How to publish your app
Authentication
In an earlier tutorial, we expanded our simple to-do list into a multi-user application, using Anvil’s built-in Users service. We want to do this same with our API – we want to authenticate our users, and only allow them to access their own data.
We can automatically require authentication for our API endpoint, using Anvil’s Users Service, by passing authenticate_users=True
to the anvil.server.http_endpoint(...)
decorator. This prevents access unless the user provides a valid username and password via standard HTTP Basic authentication.
Within our endpoint function, we can use anvil.users.get_user()
to find out what user they logged in as. Let’s modify our /tasks
endpoint so it returns only the current user’s tasks:
@anvil.server.http_endpoint('/tasks', authenticate_users=True)
def get_tasks(**q):
this_user = anvil.users.get_user()
return [{'title': task['title'], 'done': task['done'], 'id': task.get_id()}
for task in app_tables.tasks.search(owner=this_user)]
Let’s try that with curl
:
$ curl https://my-todo-list.anvil.app/_/api/tasks
Unauthorized
$ curl -u me@example.com:my_password https://my-todo-list.anvil.app/_/api/tasks
[{"done":true,"title":"Wash the car","id":"[23,567]"},{"done":true,"title":"Do the dishes","id":"[23,568]"}]
You can also see that I’ve added an id
field to the responses, containing the unique ID of each database row. Each database row has an ID, which is a string like "[23,568]"
. We can get it by calling get_id()
on the row object, and we can use it later to retrieve that row from the database.
Want to do your own authentication, rather than using Anvil’s built-in Users Service? No problem! Just pass require_credentials=True
instead of authenticate_users=True
, and then examine anvil.server.request.username
and anvil.server.request.password
for yourself.
Learn more in our reference documentation.
Accepting parameters
Now, we want an endpoint to add a new task. We use form parameters to specify the title of the new task. Form parameters are passed as keywords to the endpoint function, so to make a mandatory parameter we just make our function take an argument called title
:
@anvil.server.http_endpoint('/tasks/new',
methods=["POST"], authenticate_users=True)
def new_task(title, **q):
new_row = app_tables.tasks.add_row(title=title, done=False,
owner=anvil.users.get_user())
return {'id': new_row.get_id()}
This endpoint should only accept HTTP POST requests, so we’ve specified methods=["POST"]
. We also return the ID of the newly-created row.
Here’s how to create a new task from curl
:
$ curl -u me@example.com:my_password -d title="New task" https://my-todo-list.anvil.app/_/api/tasks/new
{"id":"[23,590]"}
Form parameters vs JSON
The example above uses “form encoding” to pass parameters. These days, API users often prefer to send JSON as the request body.
We can accommodate this, by using anvil.server.request.body_json
to decode the body of the request as a JSON object:
@anvil.server.http_endpoint('/new_task', methods=["POST"], authenticate_users=True)
def new_task(**q):
title = anvil.server.request.body_json['title']
new_row = app_tables.tasks.add_row(title=title, done=False,
owner=anvil.users.get_user())
return {'id': new_row.get_id()}
Here’s how to use this JSON-based endpoint from curl
– it’s a little more verbose:
$ curl -u me@example.com:my_password \
-X POST \
-H "Content-Type: application/json" \
-d '{"title":"New task"}' \
https://my-todo-list.anvil.app/_/api/new_task
{"id":"[23,591]"}
URL parameters
We want each task to have its own URL. We can use the unique ID of the database row for this. We’ll make each task available at /tasks/[ID]
.
To do this, we make an endpoint with a URL parameter called id
. URL parameters are passed into the endpoint function as keyword arguments, same as form parameters:
@anvil.server.http_endpoint('/tasks/:id', authenticate_users=True)
def task_url(id, **q):
this_user = anvil.users.get_user()
task = my_tasks.get_by_id(id)
if task is None or task['owner'] != this_user:
return anvil.server.HttpResponse(status=404)
else:
return {'title': task['title'], 'done': task['done'], 'id': id}
Of course, the user might request a bogus URL, with an invalid ID. Or, worse, with the ID of a row belonging to another user!
If the row with the specified ID is not available in the tasks
table, or if it is not owned by this user, our function returns an HTTP 404 (“Not Found”) response.
Let’s test it by accessing one of the tasks we saw from the listing earlier. We’ll have to URL-encode the ID so that curl will swallow it, but that’s OK: Anvil will decode the id
parameter before passing it to our function:
$ curl -u me@example.com:my_password https://my-todo-list.anvil.app/_/api/tasks/%5B23%2C567%5D
[{"done":true,"title":"Wash the car","id":"[23,567]"}
Rounding out the example: Updates and deletes
We now know enough to complete the last two operations: updating a task and deleting it. These are done by making PUT
and DELETE
requests, respectively, to the task’s url.
Let’s expand our task_url
endpoint to handle GET
, PUT
and DELETE
methods:
@anvil.server.http_endpoint('/tasks/:id',
authenticate_users=True,
methods=["GET","PUT","DELETE"])
def task_by_url(id, **p):
this_user = anvil.users.get_user()
task = my_tasks.get_by_id(id)
request = anvil.server.request
if task is None or task['owner'] != this_user:
return anvil.server.HttpResponse(status=404)
elif request.method == 'DELETE':
task.delete()
return None
elif request.method == 'PUT':
if 'title' in request.body_json:
task['title'] = request.body_json['title']
if 'done' in request.body_json:
task['done'] = request.body_json['done']
# PUT and GET both fall through to here and return the task
# as JSON
return {'title': task['title'], 'done': task['done'], 'id': id}
…and that’s a working API!
We’ve just built a fully functional, secure and authenticated API for our to-do list app – all in about 30 lines of Python.
Because Anvil is “serverless”, this API is already live on the internet, without us having to set up or maintain web servers.
You can open the source code of this app in Anvil, ready to run:
Further Reading
I’ll just finish up by mentioning a few more advanced things you might want to do with Anvil’s HTTP endpoints:
Returning different types of data
You can return plain text (strings), binary data with any content type (Media objects), or JSON (anything else).
If you return an anvil.server.HttpResponse()
object, you can control the HTTP status, headers, and content of your response.
Cookies
Anvil’s support for cookies comes with extra security. Anvil cookies are encrypted (so the user cannot see what you’ve set in a cookie) and authenticated (so the user cannot tamper with values you’ve set in a cookie).
In short, the contents of anvil.server.cookies.local[...]
and anvil.server.cookies.shared[...]
are trustworthy, unlike traditional HTTP environments.
CORS made simple
If you want your API to be accessible from other web pages, you’ll need to set CORS (Cross Origin Resource Sharing) headers. You can do this by passing cross_origin=True
to the anvil.server.http_endpoint()
decorator.
XSRF protection
HTTP endpoints in Anvil are automatically protected from XSRF (Cross-Site Request Forgery). When a request comes to an HTTP endpoint from outside your app (based on Origin
and/or Referer
headers), it executes in an independent session from the rest of your app. This prevents malicious requests from external sites from performing actions with the credentials of a logged-in user.
You can opt out of this protection: To execute cross-site requests in the same session as the rest of your app, pass cross_site_session=True
to the anvil.server.http_endpoint()
decorator.
To learn more about all these features, read more in the Anvil reference docs