REST API

flask-peewee comes with some tools for exposing your project’s models via a REST API. There are several components to the rest module, but the basic setup is to create an instance of RestAPI and then register your project’s models with subclasses of RestResource.

Each RestResource you expose via the API will support, by default, the following:

  • /api/<model name>/ – GET and POST requests

  • /api/<model name>/<primary key>/ – GET, PUT and DELETE requests

Also, you can filter results by columns on the model using django-style syntax, for example:

  • /api/blog/?name=Some%20Blog

  • /api/blog/?author__username=some_blogger

Full operations:

  • __eq: equals

  • __lt: less-than

  • __lte: less-than or equal to

  • __gt: greater-than

  • __gte: greater-than or equal to

  • __ne: not equal to

  • __in: in set

  • __is: is, ?field__is=None or ?-field__is=None for NOT NULL

  • __is_not: is not, ?field__is_not=None

  • __like: wild-card matching, case-sensitive

  • __ilike: wild-card matching, case-insensitive

  • __regexp: regular-expression matching (database-specific)

To negate an operation, prefix it with the - character, e.g. the following are equivalent:

  • /api/user/?admin=true

  • /api/user/?admin__eq=true

  • /api/user/?-admin=false

  • /api/user/?admin__ne=false

Special Python constants are supported when used as querystring parameters:

  • ?value=none translates the value to None

  • ?value=true translates the value to True

  • ?value=false translates the value to False

Getting started with the API

In this documentation we’ll start with a very simple API and build it out. The complete version of this API is included in the example app, so feel free to refer there.

The project will be a simple ‘twitter-like’ app where users can post short messages and “follow” other users.

Note

If you’re using apache with mod_wsgi and would like to use any of the auth backends that use basic auth, you will need to add the following directive: WSGIPassAuthorization On

Project models

There are three main models - User, Relationship and Message - which we will expose via the API. Here is a truncated version of what they look like:

from flask_peewee.auth import BaseUser

class User(db.Model, BaseUser):
    username = CharField()
    password = CharField()
    email = CharField()
    join_date = DateTimeField(default=datetime.datetime.now)
    active = BooleanField(default=True)
    admin = BooleanField(default=False)

class Relationship(db.Model):
    from_user = ForeignKeyField(User, backref='relationships')
    to_user = ForeignKeyField(User, backref='related_to')

class Message(db.Model):
    user = ForeignKeyField(User)
    content = TextField()
    pub_date = DateTimeField(default=datetime.datetime.now)

Creating a RestAPI

The RestAPI acts as a container for the various RestResource objects we will expose. By default it binds all resources to /api/<model-name>/.

Here we’ll create a simple api and register our models:

from flask_peewee.rest import RestAPI

from app import app # our project's Flask app

# instantiate our api wrapper
api = RestAPI(app)

# register our models so they are exposed via /api/<model>/
api.register(User)
api.register(Relationship)
api.register(Message)

# configure the urls
api.setup()

Now if we hit our project at /api/message/ we should get something like the following:

{
  "meta": {
    "model": "message",
    "next": "",
    "page_count": 1,
    "page": 1,
    "previous": ""
  },
  "objects": [
    {
      "content": "flask and peewee, together at last!",
      "pub_date": "2026-09-16T18:36:15",
      "user": 1,
      "id": 1
    },
    {
      "content": "Hey, I'm just some user",
      "pub_date": "2026-09-16T18:46:59",
      "user": 2,
      "id": 2
    }
  ]
}

Say we’re interested in the first message, we can hit /api/message/1/ to view just the details on that object:

{
  "content": "flask and peewee, together at last!",
  "pub_date": "2026-09-16T18:36:15",
  "user": 1,
  "id": 1
}

Customizing what is returned

If you access the User API endpoint, we quickly notice a problem:

$ curl http://127.0.0.1:5000/api/user/

{
  "meta": {
    "model": "user",
    "next": "",
    "page": 1,
    "page_count": 1,
    "previous": ""
  },
  "objects": [
    {
      "username": "admin",
      "admin": true,
      "email": "",
      "join_date": "2026-09-16T18:34:49",
      "active": true,
      "password": "d033e22ae348aeb5660fc2140aec35850c4da997",
      "id": 1
    },
    {
      "username": "coleifer",
      "admin": false,
      "email": "coleifer@gmail.com",
      "join_date": "2026-09-16T18:35:56",
      "active": true,
      "password": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
      "id": 2
    }
  ]
}

Passwords and email addresses are being exposed. In order to exclude these fields from serialization, subclass RestResource:

from flask_peewee.rest import RestAPI, RestResource

from app import app # our project's Flask app

# instantiate our api wrapper
api = RestAPI(app)

# create a special resource for users that excludes email and password
class UserResource(RestResource):
    exclude = ('password', 'email',)

# register our models so they are exposed via /api/<model>/
api.register(User, UserResource) # specify the UserResource
api.register(Relationship)
api.register(Message)

Now emails and passwords are no longer returned by the API.

exclude is a blacklist; its positive counterpart is fields, a whitelist of the only fields to serialize. The resource above could instead expose just the username and id:

class UserResource(RestResource):
    fields = ('username', 'id')

Reach for whichever is more convenient: fields when you want to expose a small, fixed set of columns, exclude when you want everything but a few.

Nested resources

By default a foreign key is serialized as the related row’s primary key. Notice the "user": 1 in the message output above. To embed the full related object instead, point include_resources at the resource that should render it:

class UserResource(RestResource):
    exclude = ('password', 'email',)

class MessageResource(RestResource):
    include_resources = {'user': UserResource}

api.register(User, UserResource)
api.register(Message, MessageResource)

Now each message embeds its author, serialized through UserResource (so the password and email are still excluded):

{
  "content": "flask and peewee, together at last!",
  "pub_date": "2026-09-16T18:36:15",
  "user": {
    "username": "admin",
    "admin": true,
    "active": true,
    "join_date": "2026-09-16T18:34:49",
    "id": 1
  },
  "id": 1
}

include_resources can be nested arbitrarily deep – an included resource may itself include resources – and one model can be embedded through more than one foreign key. For example, a Relationship resource can expand both endpoints:

class RelationshipResource(RestResource):
    include_resources = {
        'from_user': UserResource,
        'to_user': UserResource,
    }

The whole nested tree is loaded in a single query – one JOIN per included foreign key – so embedding related objects does not incur the N+1 queries you would get from following each row’s relations lazily.

Nested writes

Included resources also work on the way in: a POST or PUT whose body carries a nested object (instead of a bare id) creates or updates the related row as part of the same request. Two rules keep that safe:

  • A resource’s readonly_fields are stripped at every level of the payload, so a nested object cannot smuggle in a field the resource protects (e.g. slipping "admin": true into a nested user).

  • Each nested write must pass the child resource’s own check_post / check_put, exactly as a direct write to that resource would, so nesting can never be used to sidestep a resource’s authorization.

The entire object graph is saved in a single transaction, so if any nested write is rejected the whole request rolls back. To disable nested writes for a resource – silently ignoring any nested object in the payload – set nested_writes = False; the foreign key can still be assigned with a bare id.

Validating incoming data

Write payloads are validated as they are deserialized, and problems surface as a 400 with a JSON error rather than a 500 (or worse, bad data):

  • A body that is not valid JSON is rejected.

  • Values that cannot be coerced to their field’s type are rejected. This includes date/time strings: a value like "pub_date": "not-a-date" returns {"error": "Unrecognized date/time value for \"pub_date\": 'not-a-date'"} instead of being written through to the database. Both ISO-8601 (what the API itself emits) and the field’s own formats are accepted.

  • Violated database constraints (NOT NULL, unique, foreign keys) are reported as a 400 as well.

Unrecognized keys in a payload are silently ignored by default, which is forgiving but means a typo’d field name is dropped without complaint. Set reject_unknown_fields = True on the resource to get a 400 listing the offending keys instead:

class MessageResource(RestResource):
    reject_unknown_fields = True
$ curl -d '{"contnet": "hello"}' http://127.0.0.1:5000/api/message/

{"error": "Unrecognized field(s): contnet"}

Read-only fields are exempt (they are stripped, not rejected), so fetching an object and PUT-ing the whole thing back continues to work, and a foreign key may be written by field name or column name (user / user_id). Unknown keys inside a nested object are reported with the __ path notation, e.g. user__usernmae.

Allowing users to post objects

What if we want to create new messages via the Api? Or modify/delete existing messages?

$ curl -i -d '' http://127.0.0.1:5000/api/message/

HTTP/1.1 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Login Required"
Content-Type: text/html; charset=utf-8

Authentication failed

The authentication failed because the default authentication mechanism only allows read-only access.

In order to allow users to create messages via the API, we need to use a subclass of Authentication that allows POST requests. We also want to ensure that the requesting user is a member of the site.

For this we will use the UserAuthentication class as the default auth mechanism.

from auth import auth # import the Auth object used by our project

from flask_peewee.rest import RestAPI, RestResource, UserAuthentication

# create an instance of UserAuthentication
user_auth = UserAuthentication(auth)

# instantiate our api wrapper, specifying user_auth as the default
api = RestAPI(app, default_auth=user_auth)

# create a special resource for users that excludes email and password
class UserResource(RestResource):
    exclude = ('password', 'email',)

# register our models so they are exposed via /api/<model>/
api.register(User, UserResource) # specify the UserResource
api.register(Relationship)
api.register(Message)

# configure the urls
api.setup()

Now we should be able to POST new messages.

import requests

# authenticate with HTTP basic auth
resp = requests.post(
    'http://localhost:5000/api/message/',
    json={'user': 1, 'content': 'hello api'},
    auth=('admin', 'admin'),
)
response = resp.json()

The response object will look something like this:

{
  'content': 'hello api',
  'user': 1,
  'pub_date': '2026-09-22T11:25:02',
  'id': 3
}

There is a problem with this, however. Notice how the user was passed in with the POST data? This effectively will let a user post a message as another user. It also means a user can use PUT requests to modify another user’s message:

# continued from above -- edit another user's message (id=2)
resp = requests.put(
    'http://127.0.0.1:5000/api/message/2/',
    json={'content': 'haxed you, bro'},
    auth=('admin', 'admin'),
)
response = resp.json()

The response will look like this:

{
  'content': 'haxed you, bro',
  'pub_date': '2026-09-16T18:36:15',
  'user': 2,
  'id': 2
}

This is a problem – we need a way of ensuring that users can only edit their own messages. Furthermore, when they create messages we need to make sure the message is assigned to them.

Restricting API access on a per-model basis

flask-peewee comes with a special subclass of RestResource that restricts POST/PUT/DELETE requests to prevent users from modifying another user’s content.

from flask_peewee.rest import RestrictOwnerResource


class MessageResource(RestrictOwnerResource):
    owner_field = 'user'

api.register(Message, MessageResource)

Now, if we try and modify the message, we get a 403 Forbidden:

resp = requests.put(
    'http://127.0.0.1:5000/api/message/2/',
    json={'content': 'haxed you, bro'},
    auth=('admin', 'admin'),
)
print(resp.status_code)  # 403

It is fine to modify our own message, though (message with id=1):

resp = requests.put(
    'http://127.0.0.1:5000/api/message/1/',
    json={'content': 'haxed you, bro'},
    auth=('admin', 'admin'),
)
print(resp.status_code)  # 200

Under-the-hood, the implementation of the RestrictOwnerResource is pretty simple.

  • PUT / DELETE – verify the authenticated user is the owner of the object

  • POST – assign the authenticated user as the owner of the new object

Locking down a resource

Suppose we want to restrict normal users from modifying User resources. For this we can use a special subclass of UserAuthentication that restricts access to administrators:

from flask_peewee.rest import AdminAuthentication

# instantiate our user-based auth
user_auth = UserAuthentication(auth)

# instantiate admin-only auth
admin_auth = AdminAuthentication(auth)

# instantiate our api wrapper, specifying user_auth as the default
api = RestAPI(app, default_auth=user_auth)

# register the UserResource with admin auth
api.register(User, UserResource, auth=admin_auth)

Token-based authentication

UserAuthentication and AdminAuthentication use HTTP Basic auth, which is handy for humans but awkward for programmatic clients. For API clients, flask-peewee ships token-based authentication classes. Like all authentication classes they only guard the protected_methods (POST, PUT and DELETE by default – GET is open). To require auth on reads too, pass protected_methods=ALL_METHODS (a convenience constant equal to ('GET', 'POST', 'PUT', 'DELETE')) or your own list.

API keys

APIKeyAuthentication authenticates against a model with key and secret fields, supplied as query-string, header, or form parameters. The matched row is stored on g.api_key:

from flask_peewee.rest import APIKeyAuthentication

class APIKey(db.Model):
    key = CharField()
    secret = CharField()

api_key_auth = APIKeyAuthentication(APIKey)
api.register(SecretModel, auth=api_key_auth)

# curl "http://127.0.0.1:5000/api/secretmodel/?key=abc&secret=xyz"

Warning

Because the key and secret can travel in the query string, they may end up in access logs. Prefer bearer tokens (below) for anything sensitive.

Bearer tokens

BearerAuthentication reads a token from the standard Authorization: Bearer <token> header – so the credential stays out of the query string and logs – and looks it up in a model with a token field. The matched row is stored on g.api_key:

from flask_peewee.rest import BearerAuthentication

class ApiToken(db.Model):
    token = CharField()

bearer_auth = BearerAuthentication(ApiToken)
api.register(SecretModel, auth=bearer_auth)

# curl -H "Authorization: Bearer <token>" http://127.0.0.1:5000/api/secretmodel/

Override token_field to use a differently-named column, and store high-entropy tokens (override get_key to keep them hashed at rest).

Bearer tokens as users

UserBearerAuthentication resolves the token to a user and sets g.user (rather than g.api_key), so bearer tokens work with RestrictOwnerResource and anything else keyed off the authenticated user. The token model carries a foreign key to the user:

from flask_peewee.rest import UserBearerAuthentication

class ApiToken(db.Model):
    token = CharField()
    user = ForeignKeyField(User)

user_bearer_auth = UserBearerAuthentication(ApiToken)

class MessageResource(RestrictOwnerResource):
    owner_field = 'user'

api.register(Message, MessageResource, auth=user_bearer_auth)

A request carrying a valid token is now treated as that token’s user: new objects are assigned to them, and they may only modify their own. Set user_field = None if the token lives directly on the user model instead of a separate token table.

Filtering records and querying

A REST Api is not very useful if it cannot be queried in a meaningful fashion. To this end, the flask-peewee RestResource objects support “django-style” filtering:

$ curl http://127.0.0.1:5000/api/message/?user=2

This call will return only messages by the User with id=2:

{
  "meta": {
    "model": "message",
    "next": "",
    "page": 1,
    "page_count": 1,
    "previous": ""
  },
  "objects": [
    {
      "content": "haxed you, bro",
      "pub_date": "2026-09-16T18:36:15",
      "user": 2,
      "id": 2
    }
  ]
}

Joins can be traversed using the django double-underscore notation:

$ curl http://127.0.0.1:5000/api/message/?user__username=admin
{
  "meta": {
    "model": "message",
    "next": "",
    "page": 1,
    "page_count": 1,
    "previous": ""
  },
  "objects": [
    {
      "content": "flask and peewee, together at last!",
      "pub_date": "2026-09-16T18:36:15",
      "user": 1,
      "id": 1
    },
    {
      "content": "hello api",
      "pub_date": "2026-09-22T11:25:02",
      "user": 1,
      "id": 3
    }
  ]
}

It is also supported to use different comparison operators with the same double-underscore notation:

$ curl http://127.0.0.1:5000/api/user/?id__lt=2
{
  "meta": {
    "model": "user",
    "next": "",
    "page": 1,
    "page_count": 1,
    "previous": ""
},
"objects": [{
    "username": "admin",
    "admin": true,
    "email": "admin@admin",
    "active": true,
    "password": "214de$25",
    "id": 1
    }]
}
Valid Comparison Operators are:

‘eq’, ‘lt’, ‘lte’, ‘gt’, ‘gte’, ‘ne’, ‘in’, ‘is’, ‘is_not’, ‘like’, ‘ilike’, ‘regexp’

The in operator accepts a comma-separated list and/or repeated parameters, so ?id__in=1,2 and ?id__in=1&id__in=2 are equivalent.

Note

Unrecognized filter parameters – a misspelled field, or a field not exposed for filtering – are ignored rather than rejected, so a typo such as ?usernam=x silently returns every row. This is intentional: it keeps stray query-string parameters (cache-busters, tracking params, etc.) from breaking a request. Double-check your filter names if a query returns more than you expect. An unknown ordering column is likewise ignored.

Restricting what can be filtered

By default every field on the model is filterable and joins may be traversed into related models. Since filters come straight off the query string, you will often want to lock this down, especially for sensitive columns. Three RestResource attributes control it:

  • filter_fields – a whitelist; only these fields may be filtered on.

  • filter_exclude – a blacklist of fields that may never be filtered on (use __ notation for related columns, e.g. user__password).

  • filter_recursive – set to False to forbid filtering across foreign keys entirely (no user__... lookups at all).

class MessageResource(RestResource):
    # the only fields a client may filter on (with any operator)
    filter_fields = ('id', 'content', 'user__username')

class UserResource(RestResource):
    exclude = ('password',)          # don't serialize the hash...
    filter_exclude = ('password',)   # ...and don't let it be filtered on either

Because an unrecognized filter is ignored rather than rejected (see the note above), tightening this list can never break an otherwise-valid request. A now-disallowed filter stops narrowing the results.

Sorting results

Results can be sorted by specifying an ordering as a GET argument. The ordering must be a column on the model.

/api/message/?ordering=pub_date

If you would like to order objects “descending”, place a “-” (hyphen character) before the column name:

/api/message/?ordering=-pub_date

Limiting results and pagination

By default, resources are paginated 20 per-page (the paginate_by attribute). Specify a limit in the querystring to request a different page size – larger or smaller:

/api/message/?limit=2

paginate_by is only the default page size, not a maximum – a client may request a larger page. To cap how large a page can be requested, set max_paginate_by on the resource (it defaults to None, meaning no ceiling). Setting paginate_by = None disables pagination and returns every matching object on a single page (still wrapped in the standard meta/objects envelope).

In the “meta” section of the response, URIs for the “next” and “previous” sets of results are available, along with the total number of pages:

"meta": {
  "model": "message",
  "next": "/api/message/?limit=1&page=3",
  "page": 2,
  "page_count": 5,
  "previous": "/api/message/?limit=1&page=1"
}