0
# API Framework
1
2
REST API endpoints for headless CMS functionality with comprehensive serializers for pages, images, documents, and custom content. Wagtail's API framework enables decoupled front-end applications and third-party integrations.
3
4
## Capabilities
5
6
### Core API Components
7
8
Base classes and viewsets for creating REST API endpoints.
9
10
```python { .api }
11
class BaseAPIViewSet(GenericViewSet):
12
"""
13
Base viewset for Wagtail API endpoints with pagination, filtering, and authentication.
14
15
Provides common functionality for all API endpoints including:
16
- Pagination support
17
- Field filtering and expansion
18
- Search capabilities
19
- Content negotiation
20
"""
21
base_serializer_class: BaseSerializer
22
filter_backends: list
23
known_query_parameters: set
24
25
def get_queryset(self):
26
"""Get the base queryset for this endpoint."""
27
28
def get_serializer_class(self):
29
"""Get the serializer class for the current request."""
30
31
def filter_queryset(self, queryset):
32
"""Apply filters to the queryset."""
33
34
def paginate_queryset(self, queryset):
35
"""Apply pagination to the queryset."""
36
37
class PagesAPIViewSet(BaseAPIViewSet):
38
"""
39
REST API endpoint for pages with hierarchical navigation and content access.
40
41
Supports:
42
- Page listing with filtering
43
- Individual page retrieval
44
- Hierarchical navigation (children, ancestors, descendants)
45
- Content serialization with StreamField support
46
"""
47
base_serializer_class: PageSerializer
48
model: Page
49
50
def get_queryset(self):
51
"""Get live pages accessible to the current user."""
52
53
def find_view(self, request):
54
"""Find pages by slug or path."""
55
56
def listing_view(self, request):
57
"""List pages with filtering and pagination."""
58
59
def detail_view(self, request, pk):
60
"""Get individual page details."""
61
62
class ImagesAPIViewSet(BaseAPIViewSet):
63
"""
64
REST API endpoint for images with rendition generation and metadata access.
65
66
Supports:
67
- Image listing and search
68
- Image metadata access
69
- Rendition URLs for different sizes
70
- Collection-based filtering
71
"""
72
base_serializer_class: ImageSerializer
73
model: Image
74
75
def get_queryset(self):
76
"""Get images accessible to the current user."""
77
78
class DocumentsAPIViewSet(BaseAPIViewSet):
79
"""
80
REST API endpoint for documents with download URLs and metadata.
81
82
Supports:
83
- Document listing and search
84
- Document metadata access
85
- Download URLs
86
- Collection-based filtering
87
"""
88
base_serializer_class: DocumentSerializer
89
model: Document
90
91
def get_queryset(self):
92
"""Get documents accessible to the current user."""
93
```
94
95
### Serializers
96
97
Serializer classes for converting models to JSON representations.
98
99
```python { .api }
100
class BaseSerializer(serializers.ModelSerializer):
101
"""
102
Base serializer for API responses with field selection and expansion.
103
104
Features:
105
- Dynamic field selection via 'fields' parameter
106
- Field expansion for related objects
107
- Consistent error handling
108
"""
109
def __init__(self, *args, **kwargs):
110
"""Initialize serializer with dynamic field configuration."""
111
112
def to_representation(self, instance):
113
"""Convert model instance to dictionary representation."""
114
115
class PageSerializer(BaseSerializer):
116
"""
117
Serializer for Page models with content and metadata serialization.
118
119
Handles:
120
- Page hierarchy information
121
- StreamField content serialization
122
- Rich text field processing
123
- Image and document references
124
"""
125
id: int
126
meta: dict
127
title: str
128
html_url: str
129
slug: str
130
url_path: str
131
seo_title: str
132
search_description: str
133
show_in_menus: bool
134
first_published_at: datetime
135
last_published_at: datetime
136
137
def get_meta(self, instance):
138
"""Get metadata about the page type and properties."""
139
140
def get_html_url(self, instance):
141
"""Get the full HTML URL for this page."""
142
143
class ImageSerializer(BaseSerializer):
144
"""
145
Serializer for Image models with rendition support.
146
147
Provides:
148
- Image metadata (dimensions, file size, etc.)
149
- Rendition generation for different sizes
150
- Collection and tag information
151
"""
152
id: int
153
meta: dict
154
title: str
155
original: dict
156
thumbnail: dict
157
width: int
158
height: int
159
created_at: datetime
160
161
def get_original(self, instance):
162
"""Get original image URL and metadata."""
163
164
def get_thumbnail(self, instance):
165
"""Get thumbnail rendition URL."""
166
167
class DocumentSerializer(BaseSerializer):
168
"""
169
Serializer for Document models with download information.
170
171
Provides:
172
- Document metadata (file size, type, etc.)
173
- Download URLs
174
- Collection information
175
"""
176
id: int
177
meta: dict
178
title: str
179
download_url: str
180
created_at: datetime
181
182
def get_download_url(self, instance):
183
"""Get secure download URL for this document."""
184
```
185
186
### API Configuration
187
188
Classes for configuring API field exposure and customization.
189
190
```python { .api }
191
class APIField:
192
"""
193
Configuration for exposing model fields through the API.
194
195
Controls how model fields are serialized and what data is included.
196
"""
197
def __init__(self, name, serializer=None):
198
"""
199
Initialize API field configuration.
200
201
Parameters:
202
name (str): Name of the field to expose
203
serializer (Serializer): Custom serializer for this field
204
"""
205
206
class WagtailAPIRouter:
207
"""
208
URL router for configuring API endpoints and routing.
209
210
Manages URL patterns and endpoint registration for the API.
211
"""
212
def __init__(self, name='wagtailapi'):
213
"""
214
Initialize API router.
215
216
Parameters:
217
name (str): Name for the API URL namespace
218
"""
219
220
def register_endpoint(self, prefix, viewset):
221
"""
222
Register an API endpoint.
223
224
Parameters:
225
prefix (str): URL prefix for the endpoint
226
viewset (ViewSet): ViewSet class handling the endpoint
227
"""
228
229
@property
230
def urls(self):
231
"""Get URL patterns for all registered endpoints."""
232
```
233
234
### API Filters
235
236
Filter classes for querying and searching API endpoints.
237
238
```python { .api }
239
class FieldsFilter:
240
"""
241
Filter for selecting specific fields in API responses.
242
243
Allows clients to request only the fields they need.
244
"""
245
def filter_queryset(self, request, queryset, view):
246
"""Apply field selection to the response."""
247
248
class ChildOfFilter:
249
"""
250
Filter for finding pages that are children of a specific page.
251
"""
252
def filter_queryset(self, request, queryset, view):
253
"""Filter to pages that are children of specified parent."""
254
255
class DescendantOfFilter:
256
"""
257
Filter for finding pages that are descendants of a specific page.
258
"""
259
def filter_queryset(self, request, queryset, view):
260
"""Filter to pages that are descendants of specified ancestor."""
261
262
class OrderingFilter:
263
"""
264
Filter for ordering API results by specified fields.
265
"""
266
def filter_queryset(self, request, queryset, view):
267
"""Apply ordering to the queryset."""
268
269
class SearchFilter:
270
"""
271
Filter for full-text search across API endpoints.
272
"""
273
def filter_queryset(self, request, queryset, view):
274
"""Apply search query to the queryset."""
275
```
276
277
### API Utilities
278
279
Utility functions and classes for API functionality.
280
281
```python { .api }
282
def get_base_url(request=None):
283
"""
284
Get the base URL for API endpoints.
285
286
Parameters:
287
request (HttpRequest): Current request object
288
289
Returns:
290
str: Base URL for the API
291
"""
292
293
def get_object_detail_url(context, model, pk, url_name=None):
294
"""
295
Get detail URL for an API object.
296
297
Parameters:
298
context (dict): Serializer context
299
model (Model): Model class
300
pk (int): Primary key of the object
301
url_name (str): URL name for the endpoint
302
303
Returns:
304
str: Detail URL for the object
305
"""
306
307
class BadRequestError(Exception):
308
"""Exception for API bad request errors."""
309
310
class NotFoundError(Exception):
311
"""Exception for API not found errors."""
312
```
313
314
## Usage Examples
315
316
### Setting Up API Endpoints
317
318
```python
319
# urls.py
320
from wagtail.api.v2.router import WagtailAPIRouter
321
from wagtail.api.v2.views import PagesAPIViewSet
322
from wagtail.images.api.v2.views import ImagesAPIViewSet
323
from wagtail.documents.api.v2.views import DocumentsAPIViewSet
324
325
# Create API router
326
api_router = WagtailAPIRouter('wagtailapi')
327
328
# Register default endpoints
329
api_router.register_endpoint('pages', PagesAPIViewSet)
330
api_router.register_endpoint('images', ImagesAPIViewSet)
331
api_router.register_endpoint('documents', DocumentsAPIViewSet)
332
333
# Add to URL patterns
334
urlpatterns = [
335
path('admin/', admin.site.urls),
336
path('api/v2/', api_router.urls),
337
path('', include(wagtail_urls)),
338
]
339
```
340
341
### Custom API Endpoints
342
343
```python
344
from wagtail.api.v2.views import BaseAPIViewSet
345
from wagtail.api.v2.serializers import BaseSerializer
346
from rest_framework.fields import CharField, DateTimeField
347
from myapp.models import BlogPage, Author
348
349
class AuthorSerializer(BaseSerializer):
350
"""Custom serializer for Author model."""
351
name = CharField(read_only=True)
352
bio = CharField(read_only=True)
353
posts_count = serializers.SerializerMethodField()
354
355
def get_posts_count(self, instance):
356
"""Get number of blog posts by this author."""
357
return BlogPage.objects.filter(author=instance).count()
358
359
class AuthorAPIViewSet(BaseAPIViewSet):
360
"""Custom API endpoint for authors."""
361
base_serializer_class = AuthorSerializer
362
filter_backends = [SearchFilter, OrderingFilter]
363
model = Author
364
known_query_parameters = BaseAPIViewSet.known_query_parameters.union([
365
'name', 'bio'
366
])
367
368
def get_queryset(self):
369
return Author.objects.all()
370
371
# Register custom endpoint
372
api_router.register_endpoint('authors', AuthorAPIViewSet)
373
```
374
375
### Configuring Page API Fields
376
377
```python
378
from wagtail.api.v2.serializers import BaseSerializer
379
from wagtail.api import APIField
380
from wagtail.models import Page
381
from wagtail.fields import StreamField
382
from rest_framework.fields import CharField
383
384
class BlogPage(Page):
385
"""Blog page with API field configuration."""
386
date = models.DateField("Post date")
387
intro = models.CharField(max_length=250)
388
body = StreamField([
389
('heading', CharBlock()),
390
('paragraph', RichTextBlock()),
391
('image', ImageChooserBlock()),
392
])
393
author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
394
395
# Configure API field exposure
396
api_fields = [
397
APIField('date'),
398
APIField('intro'),
399
APIField('body'), # StreamField automatically serialized
400
APIField('author', serializer=AuthorSerializer()),
401
]
402
403
content_panels = Page.content_panels + [
404
FieldPanel('date'),
405
FieldPanel('intro'),
406
FieldPanel('body'),
407
FieldPanel('author'),
408
]
409
410
# Custom serializer for complex fields
411
class BlogPageSerializer(BaseSerializer):
412
"""Custom serializer for blog pages."""
413
reading_time = serializers.SerializerMethodField()
414
related_posts = serializers.SerializerMethodField()
415
416
def get_reading_time(self, instance):
417
"""Calculate estimated reading time."""
418
word_count = len(str(instance.body).split())
419
return max(1, word_count // 200) # Assume 200 words per minute
420
421
def get_related_posts(self, instance):
422
"""Get related blog posts."""
423
related = BlogPage.objects.live().exclude(pk=instance.pk)[:3]
424
return [{'title': post.title, 'url': post.url} for post in related]
425
```
426
427
### Making API Requests
428
429
```python
430
import requests
431
432
# Base API URL
433
base_url = 'https://example.com/api/v2/'
434
435
# Get all pages
436
response = requests.get(f'{base_url}pages/')
437
pages = response.json()
438
439
# Get specific page
440
page_id = 123
441
response = requests.get(f'{base_url}pages/{page_id}/')
442
page = response.json()
443
444
# Search pages
445
response = requests.get(f'{base_url}pages/', params={
446
'search': 'django tutorial',
447
'type': 'blog.BlogPage',
448
'fields': 'title,slug,date,author'
449
})
450
search_results = response.json()
451
452
# Get page children
453
parent_id = 10
454
response = requests.get(f'{base_url}pages/', params={
455
'child_of': parent_id,
456
'limit': 20
457
})
458
children = response.json()
459
460
# Get images with renditions
461
response = requests.get(f'{base_url}images/')
462
images = response.json()
463
464
# Get specific image
465
image_id = 456
466
response = requests.get(f'{base_url}images/{image_id}/')
467
image = response.json()
468
print(f"Original: {image['meta']['download_url']}")
469
print(f"Thumbnail: {image['meta']['thumbnail']['url']}")
470
471
# Filter by collection
472
collection_id = 5
473
response = requests.get(f'{base_url}images/', params={
474
'collection_id': collection_id
475
})
476
collection_images = response.json()
477
```
478
479
### JavaScript Frontend Integration
480
481
```javascript
482
// API client class
483
class WagtailAPI {
484
constructor(baseURL) {
485
this.baseURL = baseURL;
486
}
487
488
async getPages(params = {}) {
489
const url = new URL(`${this.baseURL}pages/`);
490
Object.keys(params).forEach(key => {
491
url.searchParams.append(key, params[key]);
492
});
493
494
const response = await fetch(url);
495
return response.json();
496
}
497
498
async getPage(id, fields = []) {
499
const url = new URL(`${this.baseURL}pages/${id}/`);
500
if (fields.length > 0) {
501
url.searchParams.append('fields', fields.join(','));
502
}
503
504
const response = await fetch(url);
505
return response.json();
506
}
507
508
async searchPages(query, type = null) {
509
return this.getPages({
510
search: query,
511
...(type && { type: type })
512
});
513
}
514
515
async getImages(params = {}) {
516
const url = new URL(`${this.baseURL}images/`);
517
Object.keys(params).forEach(key => {
518
url.searchParams.append(key, params[key]);
519
});
520
521
const response = await fetch(url);
522
return response.json();
523
}
524
}
525
526
// Usage example
527
const api = new WagtailAPI('https://example.com/api/v2/');
528
529
// Load blog posts
530
async function loadBlogPosts() {
531
try {
532
const data = await api.getPages({
533
type: 'blog.BlogPage',
534
fields: 'title,slug,date,intro,author',
535
limit: 10,
536
order: '-date'
537
});
538
539
const posts = data.items.map(post => ({
540
title: post.title,
541
slug: post.slug,
542
date: post.date,
543
intro: post.intro,
544
author: post.author?.name || 'Unknown',
545
url: post.meta.html_url
546
}));
547
548
renderBlogPosts(posts);
549
} catch (error) {
550
console.error('Failed to load blog posts:', error);
551
}
552
}
553
554
// Search functionality
555
async function searchContent(query) {
556
try {
557
const [pages, images] = await Promise.all([
558
api.searchPages(query),
559
api.getImages({ search: query })
560
]);
561
562
return {
563
pages: pages.items,
564
images: images.items
565
};
566
} catch (error) {
567
console.error('Search failed:', error);
568
}
569
}
570
```
571
572
### Advanced API Customization
573
574
```python
575
from wagtail.api.v2.views import BaseAPIViewSet
576
from wagtail.api.v2.filters import BaseFilterSet
577
from django_filters import rest_framework as filters
578
579
class BlogPageFilter(BaseFilterSet):
580
"""Custom filter for blog pages."""
581
date_from = filters.DateFilter(field_name='date', lookup_expr='gte')
582
date_to = filters.DateFilter(field_name='date', lookup_expr='lte')
583
author_name = filters.CharFilter(field_name='author__name', lookup_expr='icontains')
584
585
class Meta:
586
model = BlogPage
587
fields = ['date_from', 'date_to', 'author_name']
588
589
class BlogPageAPIViewSet(PagesAPIViewSet):
590
"""Enhanced blog page API with custom filtering."""
591
filter_backends = PagesAPIViewSet.filter_backends + [filters.DjangoFilterBackend]
592
filterset_class = BlogPageFilter
593
594
known_query_parameters = PagesAPIViewSet.known_query_parameters.union([
595
'date_from', 'date_to', 'author_name'
596
])
597
598
def get_queryset(self):
599
return BlogPage.objects.live().select_related('author')
600
601
# Custom endpoint with authentication
602
from rest_framework.authentication import TokenAuthentication
603
from rest_framework.permissions import IsAuthenticated
604
605
class PrivateContentAPIViewSet(BaseAPIViewSet):
606
"""API endpoint requiring authentication."""
607
authentication_classes = [TokenAuthentication]
608
permission_classes = [IsAuthenticated]
609
610
def get_queryset(self):
611
# Return content based on user permissions
612
user = self.request.user
613
return Page.objects.live().filter(
614
owner=user
615
)
616
```
617
618
### API Response Caching
619
620
```python
621
from django.views.decorators.cache import cache_page
622
from django.utils.decorators import method_decorator
623
624
@method_decorator(cache_page(60 * 15), name='listing_view') # 15 minutes
625
@method_decorator(cache_page(60 * 60), name='detail_view') # 1 hour
626
class CachedPagesAPIViewSet(PagesAPIViewSet):
627
"""API viewset with response caching."""
628
629
def get_queryset(self):
630
# Add cache invalidation logic
631
return super().get_queryset().select_related('locale')
632
633
# Custom cache invalidation
634
from django.core.cache import cache
635
from wagtail.signals import page_published, page_unpublished
636
637
def invalidate_api_cache(sender, **kwargs):
638
"""Invalidate API cache when pages are published/unpublished."""
639
cache.delete_pattern('views.decorators.cache.cache_page.*')
640
641
page_published.connect(invalidate_api_cache)
642
page_unpublished.connect(invalidate_api_cache)
643
```