RESTful API#
FairDM automatically generates a fully documented RESTful API for every model you register. You do not need to write any Django REST Framework views, serializers, routers, or URL patterns — the framework handles everything.
Automatic Endpoint Generation#
When you register a Sample or Measurement model, FairDM creates the following endpoints automatically:
GET /api/v1/samples/<model-slug>/ — list all publicly visible records
GET /api/v1/samples/<model-slug>/{uuid}/ — detail for a specific record
POST /api/v1/samples/<model-slug>/ — create (authenticated users only)
PATCH /api/v1/samples/<model-slug>/{uuid}/ — partial update (authorized users only)
DELETE /api/v1/samples/<model-slug>/{uuid}/ — delete (authorized users only)
The <model-slug> is derived from your model’s verbose_name_plural (lowercased, spaces replaced with hyphens). For example, a model with verbose_name_plural = "rock samples" becomes rock-samples. See URL Slugs and verbose_name_plural for details.
Core model endpoints are also available:
Endpoint |
Methods |
|---|---|
|
GET, POST |
|
GET, PATCH, DELETE |
|
GET, POST |
|
GET, PATCH, DELETE |
|
GET |
|
GET |
|
GET (discovery catalog) |
|
GET (discovery catalog) |
Discovery Catalog#
GET /api/v1/samples/ and GET /api/v1/measurements/ return a machine-readable catalog of all registered types:
{
"types": [
{
"name": "RockSample",
"verbose_name": "Rock Sample",
"endpoint": "/api/v1/samples/rock-sample/",
"fields": ["name", "location", "date_collected"],
"filterable_fields": ["location", "date_collected"],
"count": 42
}
]
}
The count field reflects only records visible to the requesting user (public records for anonymous users, additional private records for authenticated users with guardian permissions).
Interactive Documentation#
FairDM ships a Swagger UI and ReDoc interface powered by drf-spectacular:
URL |
URL name |
Description |
|---|---|---|
|
|
Swagger UI — try endpoints interactively |
|
|
ReDoc — clean reference documentation |
|
|
Raw OpenAPI 3.0 schema (YAML) |
Authentication#
Obtaining a Token#
POST /api/v1/auth/login/
Content-Type: application/json
{"email": "user@example.com", "password": "secret"}
Response:
{"key": "abc123def456..."}
Using the Token#
Include the token in the Authorization header of every authenticated request:
GET /api/v1/projects/
Authorization: Token abc123def456...
Session Authentication#
Browser-based session authentication is also supported (used automatically by the Swagger UI “Authorize” button).
Logging Out#
POST /api/v1/auth/logout/
Authorization: Token abc123def456...
Permission Model#
FairDM’s API enforces the same object-level permission model as the web interface:
Scenario |
Result |
|---|---|
Anonymous GET on public object |
200 OK |
Anonymous GET on private object |
404 Not Found (non-disclosure) |
Anonymous POST/PATCH/DELETE |
401 Unauthorized |
Authenticated GET on private object without view perm |
404 Not Found |
Authenticated PATCH on public object without change perm |
403 Forbidden |
Authenticated PATCH on private object without any perm |
404 Not Found |
Owner (has guardian perms) on any operation |
200/201/204 OK |
Non-disclosure (404 instead of 403) is used for unauthorized access to detail endpoints to avoid leaking whether a private object exists.
Permission Assignment on Create#
When you create an object via the API, FairDM’s ObjectPermissionsAssignmentMixin automatically assigns guardian object permissions (view_*, change_*, delete_*) to the requesting user, making the creator the object owner.
Customizing Serializer Fields#
FairDM uses a three-tier resolution to determine which fields appear in API responses:
Tier 1 — fields (default)#
If only fields is specified in the registry config, those fields are used for the API serializer:
@fairdm.register
class RockSampleConfig(ModelConfiguration):
model = RockSample
fields = ["name", "location", "date_collected", "notes"]
# API will expose: url, name, location, date_collected, notes
Tier 2 — serializer_fields (API-specific override)#
Use serializer_fields when you want a different set of fields for the API compared to tables/forms:
@fairdm.register
class RockSampleConfig(ModelConfiguration):
model = RockSample
fields = ["name", "location", "date_collected", "notes"] # for forms/tables
serializer_fields = ["name", "location", "date_collected", "uuid"] # API-only subset
Tier 3 — serializer_class (full custom override)#
For complete control, provide your own serializer class. Custom serializers must subclass BaseSampleSerializer (or BaseMeasurementSerializer for Measurement models) — omitting this will raise a django.core.exceptions.ImproperlyConfigured error at startup.
from fairdm.api.serializers import BaseSampleSerializer
class RockSampleSerializer(BaseSampleSerializer):
class Meta(BaseSampleSerializer.Meta):
model = RockSample
fields = BaseSampleSerializer.Meta.fields + ["location", "date_collected", "lab_code"]
read_only_fields = ["lab_code"]
@fairdm.register
class RockSampleConfig(ModelConfiguration):
model = RockSample
serializer_class = RockSampleSerializer
Extending Meta from the base class preserves the mandatory fields (url, uuid, name, status, dataset, added, modified, polymorphic_ctype) that FairDM depends on. You can add, reorder, or override fields — but you cannot remove the mandatory ones without risking broken API clients.
Warning
Passing a serializer that does not inherit from BaseSampleSerializer or BaseMeasurementSerializer raises:
ImproperlyConfigured: RockSampleSerializer must subclass BaseSampleSerializer.
URL Slugs and verbose_name_plural#
FairDM derives the <model-slug> component of every endpoint from the model’s verbose_name_plural metadata (lowercased, spaces → hyphens). This gives you full control over URL structure without touching router configuration.
Default derivation#
The verbose_name_plural is set automatically by Django using the Meta.verbose_name (or the class name if not specified):
Model class |
Derived slug |
|---|---|
|
|
|
|
|
|
Overriding the slug#
Set Meta.verbose_name_plural in your model class:
class ThinSection(Sample):
"""Petrographic thin section sample."""
class Meta(Sample.Meta):
verbose_name = "thin section"
verbose_name_plural = "thin sections" # → endpoint: /api/v1/samples/thin-sections/
Migration note for existing portals#
If you are upgrading from a FairDM version that used CamelCase-decomposed slugs, your URL names and endpoint paths have changed. The table below shows the old and new slugs for common patterns:
Model class |
Old slug (CamelCase) |
New slug (verbose_name_plural) |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Update any hardcoded API clients, {% url %} references, or OpenAPI schema snapshots after upgrading.
Extending the Router with Custom Viewsets#
If you need a custom viewset for a specific model, you can extend the FairDM router in your portal’s urls.py:
from fairdm.api.router import fairdm_api_router
from fairdm.api.viewsets import BaseViewSet
from myportal.models import SpecialSample
from myportal.serializers import SpecialSampleSerializer
class SpecialSampleViewSet(BaseViewSet):
queryset = SpecialSample.objects.all()
serializer_class = SpecialSampleSerializer
fairdm_api_router.register(r"samples/special-sample", SpecialSampleViewSet, basename="special-sample")
Rate Limiting#
The API enforces the following default throttle rates:
User type |
Default rate |
|---|---|
Anonymous |
100 requests/hour |
Authenticated |
1000 requests/hour |
Portal operators can override these in their settings:
# In your portal's settings.py
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
"anon": "50/hour",
"user": "500/hour",
}
Throttled requests receive a 429 Too Many Requests response with a Retry-After header indicating when the quota resets.
CORS#
The API restricts cross-origin access by default. To allow specific origins (e.g., for a JavaScript frontend):
# In your portal's settings.py
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
"https://my-portal-frontend.example.com",
]
OpenAPI Schema Customisation#
Override the API title, description, and version in your portal settings:
SPECTACULAR_SETTINGS = {
"TITLE": "My Research Portal API",
"DESCRIPTION": "RESTful API for the My Research Portal research data portal.",
"VERSION": "2.0.0",
}
Customizing the API Description#
FairDM ships with a rich default API description and title that appear in Swagger UI and the raw OpenAPI schema. You can override them without modifying the framework:
# In your portal's settings.py
FAIRDM_API_TITLE = "Palaeo-Climate Data Portal API"
FAIRDM_API_DESCRIPTION = """\
## Palaeo-Climate Data Portal API
Programmatic access to all published palaeo-climate data in this portal.
### Resources
- `/api/v1/projects/` — Research projects
- `/api/v1/datasets/` — Sampled datasets
- `/api/v1/samples/` — Discover all sample types
See the [Developer Guide](https://my-portal.example.com/docs/api/) for full details.
"""
FairDM merges these settings into SPECTACULAR_SETTINGS automatically at startup. You can also override SPECTACULAR_SETTINGS['VERSION'] directly if you need to change the API version string.
Schema Naming Conventions#
FairDM auto-generates serializer classes for all registered models. The serializer class names follow the pattern {ModelName}Serializer (e.g. RockSampleSerializer, XRFMeasurementSerializer). drf-spectacular strips the Serializer suffix when deriving OpenAPI schema component names, so the components appear as clean names in Swagger:
Model class |
Serializer class |
Schema component |
|---|---|---|
|
|
|
|
|
|
|
|
|
Patched* components (from COMPONENT_SPLIT_PATCH=True) follow the same clean naming pattern:
Operation |
Schema component |
|---|---|
|
|
Note
Migration note: If you were code-generating API clients from an older FairDM OpenAPI schema,
schema component names have changed — RockSampleAPI → RockSample, PatchedProjectAPI →
PatchedProject. Regenerate your client code after upgrading.
Model Descriptions in the API Docs#
Swagger UI shows a description for each endpoint group. For auto-generated viewsets, FairDM resolves the description from the registry configuration using the following priority order:
ModelConfiguration.description(top-level attribute)ModelConfiguration.metadata.description(fromModelMetadata)Model class docstring
Fallback:
"Endpoints for managing {verbose_name_plural}."
To provide a meaningful description visible in Swagger, add it to your registry config:
@fairdm.register
class RockSampleConfig(ModelConfiguration):
model = RockSample
metadata = ModelMetadata(
description=(
"Geological rock samples collected from field sites. Each sample records "
"lithology, collection date, weight, and mineralogical observations."
),
# ... other metadata ...
)
fields = [...]
The description is displayed in Swagger UI when users expand the endpoint group for RockSample.