Skip to content

Commit 45ca5ab

Browse files
committedApr 10, 2025
Update api docs
1 parent 7f30017 commit 45ca5ab

File tree

1 file changed

+117
-7
lines changed

1 file changed

+117
-7
lines changed
 

Diff for: ‎plain-api/plain/api/README.md

+117-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
**Build APIs using class-based views.**
44

5-
The `plain.api` package provides lightweight view classes for building APIs using the same patterns as regular views. It also provides an `APIKey` model and support for generating [OpenAPI](https://www.openapis.org/) documents.
5+
This package includes lightweight view classes for building APIs using the same patterns as regular HTML views. It also provides an [`APIKey` model](#api-keys) and support for generating [OpenAPI](#openapi) documents.
6+
7+
Because [Views](/plain/plain/views/README.md) can convert built-in types to responses, an API view can simply return a dict or list to send a JSON response back to the client. More complex responses can use the [`JsonResponse`](/plain/plain/http/response.py#JsonResponse) class.
68

79
```python
810
# app/api/views.py
@@ -14,6 +16,7 @@ from app.users.models import User
1416
from app.pullrequests.models import PullRequest
1517

1618

19+
# An example base class that will be used across your custom API
1720
class BaseAPIView(APIView, APIKeyView):
1821
def use_api_key(self):
1922
super().use_api_key()
@@ -29,6 +32,7 @@ class BaseAPIView(APIView, APIKeyView):
2932
)
3033

3134

35+
# An endpoint that returns the current user
3236
class UserView(BaseAPIView):
3337
def get(self):
3438
return {
@@ -38,6 +42,7 @@ class UserView(BaseAPIView):
3842
}
3943

4044

45+
# An endpoint that filters querysets based on the user
4146
class PullRequestView(BaseAPIView):
4247
def get(self):
4348
try:
@@ -64,6 +69,8 @@ class PullRequestView(BaseAPIView):
6469
}
6570
```
6671

72+
URLs work like they do everywhere else, though it's generally recommended to put everything together into an `app.api` package and `api` namespace.
73+
6774
```python
6875
# app/api/urls.py
6976
from plain.urls import Router, path
@@ -81,15 +88,98 @@ class APIRouter(Router):
8188

8289
## Authentication and authorization
8390

84-
TODO
91+
Handling authentication in the API is pretty straightforward. If you use [API keys](#api-keys), then the `APIKeyView` will parse the `Authorization` header and set `self.api_key`. You will then customize the `use_api_key` method to associate the request with a user (or team, for example), depending on how your app works. To perform custom authentication, you can create your own base view class and hook into [`View.get_response`](/plain/plain/views/base.py#get_response).
92+
93+
```python
94+
class BaseAPIView(APIView, APIKeyView):
95+
def use_api_key(self):
96+
super().use_api_key()
97+
98+
if user := self.api_key.users.first():
99+
self.request.user = user
100+
else:
101+
raise ResponseException(
102+
JsonResponse(
103+
{"error": "API key not associated with a user."},
104+
status_code=403,
105+
)
106+
)
107+
```
108+
109+
When it comes to authorizing actions, typically you will factor this in to the queryset to only return objects that the user is allowed to see. If a response method (`get`, `post`, etc.) returns `None`, then the view will return a 404 response. Other status codes can be returned with an int (ex. `403`) or a `JsonResponse` object.
110+
111+
```python
112+
class PullRequestView(BaseAPIView):
113+
def get(self):
114+
try:
115+
pull = (
116+
PullRequest.objects.all()
117+
.visible_to_user(self.request.user)
118+
.get(uuid=self.url_kwargs["uuid"])
119+
)
120+
except PullRequest.DoesNotExist:
121+
return None
122+
123+
# ...return the authorized data here
124+
```
125+
126+
## `PUT`, `POST`, and `PATCH`
127+
128+
One way to handle PUT, POST, and PATCH endpoints is to use standard [forms](/plain/plain/forms/README.md). This will use the same validation and error handling as an HTML form, but will parse the input from the JSON request instead of HTML form data.
129+
130+
```python
131+
class UserForm(ModelForm):
132+
class Meta:
133+
model = User
134+
fields = [
135+
"username",
136+
"time_zone",
137+
]
138+
139+
class UserView(BaseAPIView):
140+
def patch(self):
141+
form = UserForm(
142+
request=self.request,
143+
instance=self.request.user,
144+
)
145+
146+
if form.is_valid():
147+
user = form.save()
148+
return {
149+
"uuid": user.uuid,
150+
"username": user.username,
151+
"time_zone": str(user.time_zone),
152+
}
153+
else:
154+
return {"errors": form.errors}
155+
```
156+
157+
If you don't want to use Plain's forms, you could also use a third-party schema/validation library like [Pydantic](https://docs.pydantic.dev/latest/) or [Marshmallow](https://marshmallow.readthedocs.io/en/3.x-line/). But depending on your use case, you may not need to use forms or fancy validation at all!
158+
159+
## `DELETE`
160+
161+
Deletes can be handled in the `delete` method of the view. Most of the time this just means getting the object, deleting it, and returning a 204.
162+
163+
```python
164+
class PullRequestView(BaseAPIView):
165+
def delete(self):
166+
try:
167+
pull = (
168+
PullRequest.objects.all()
169+
.visible_to_user(self.request.user)
170+
.get(uuid=self.url_kwargs["uuid"])
171+
)
172+
except PullRequest.DoesNotExist:
173+
return None
85174

86-
## Forms
175+
pull.delete()
87176

88-
TODO
177+
return 204
178+
```
89179

90180
## API keys
91181

92-
The provided [`APIKey` model](./models.py) includes randomly generated, unique API tokens that are automatically parsed by `APIAuthViewMixin`. The tokens can optionally be named and include an `expires_at` date.
182+
The provided [`APIKey` model](./models.py) includes randomly generated, unique API tokens that are automatically parsed by `APIKeyView`. The tokens can optionally be named and include an `expires_at` date.
93183

94184
Associating an `APIKey` with a user (or team, for example) is up to you. Most likely you will want to use a `ForeignKey` or a `ManyToManyField`.
95185

@@ -128,8 +218,28 @@ user.api_key = APIKey.objects.create(name="Example")
128218
user.save()
129219
```
130220

131-
If your API key is associated with something other than a user, or does not use the related name `"users"`, you can define your own [`associate_api_key`](./views.py#associate_api_key) method.
221+
To use API keys in your views, you can inherit from `APIKeyView` and customize the [`use_api_key` method](./views.py#use_api_key) to set the `request.user` attribute (or any other attribute) to the object associated with the API key.
222+
223+
```python
224+
# app/api/views.py
225+
from plain.api.views import APIKeyView, APIView
226+
227+
228+
class BaseAPIView(APIView, APIKeyView):
229+
def use_api_key(self):
230+
super().use_api_key()
231+
232+
if user := self.api_key.users.first():
233+
self.request.user = user
234+
else:
235+
raise ResponseException(
236+
JsonResponse(
237+
{"error": "API key not associated with a user."},
238+
status_code=403,
239+
)
240+
)
241+
```
132242

133243
## OpenAPI
134244

135-
TODO
245+
https://www.openapis.org/

0 commit comments

Comments
 (0)