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}

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:

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 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_auth=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