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=Noneor?-field__is=Nonefor 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=nonetranslates the value toNone?value=truetranslates the value toTrue?value=falsetranslates the value toFalse
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_fieldsare stripped at every level of the payload, so a nested object cannot smuggle in a field the resource protects (e.g. slipping"admin": trueinto 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 ownformatsare 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 toFalseto forbid filtering across foreign keys entirely (nouser__...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"
}