REST API Patterns
This document explains how we structure API endpoints, serializers, views, and URLs.
Request Flow
The authenticated API request flow is:
URL
-> Authentication
-> Organization membership and capability
-> View
-> Serializer
-> Model
-> DatabaseAnd on the way back:
Database
-> Model
-> Serializer
-> JSON ResponseSerializers
Serializers convert between Django models and JSON.
Example file:
src/organizations/serializers.pyExample:
from rest_framework import serializers
from .models import Organization
class OrganizationSerializer(serializers.ModelSerializer):
current_user_role = serializers.SerializerMethodField()
class Meta:
model = Organization
fields = [
"id",
"name",
"slug",
"current_user_role",
"created_at",
"updated_at",
]Why:
ModelSerializerreads field definitions from the model.fieldscontrols what the API exposes.- The serializer validates input for
POST,PATCH, etc.
Views
Views define API behavior.
Example file:
src/organizations/api/workspaces.pyOrganization-owned collection views first resolve the organization through the authenticated user's membership. A simplified example is:
class CompanyListCreateAPIView(APIView):
def get(self, request, organization_id):
organization = get_organization_for_user(
user=request.user,
organization_id=organization_id,
capability=Capability.VIEW_CRM,
)
companies = Company.objects.filter(organization=organization)
serializer = CompanySerializer(companies, many=True)
return Response(serializer.data)
def post(self, request, organization_id):
organization = get_organization_for_user(
user=request.user,
organization_id=organization_id,
capability=Capability.MANAGE_CRM,
)
serializer = CompanySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(organization=organization)
return Response(serializer.data, status=201)Why:
- The URL supplies the tenant identifier.
- The membership lookup both authorizes and resolves the organization.
- The queryset is filtered by that resolved organization.
- The server supplies
organizationduring save; clients cannot select a different tenant in JSON. many=Truemeans the serializer receives many objects, not one.serializer.is_valid()validates request data.serializer.save()creates the database row.Response(...)returns JSON.
App URLs
App URLs connect app routes to views.
Example file:
src/organizations/urls.pyExample:
urlpatterns = [
path("", OrganizationListCreateAPIView.as_view(), name="organization-list"),
path("<uuid:organization_id>/", OrganizationDetailAPIView.as_view(), name="organization-detail"),
]Why:
""means the root of the app route.- The global project URL decides the prefix.
.as_view()turns the class into a Django-compatible view.<uuid:organization_id>/captures one resource id from the URL.
Project URLs
Project URLs connect global API prefixes to app URLs.
Example file:
src/bubllio_crm/urls.pyExample:
urlpatterns = [
path("admin/", admin.site.urls),
path("api-auth/", include("rest_framework.urls")),
path("api/v1/organizations/", include("organizations.urls")),
]This creates:
/api/v1/organizations/
/api/v1/organizations/<organization_id>/companies/
/api/v1/organizations/<organization_id>/contacts/
/api/v1/organizations/<organization_id>/automations/Class Splitting Logic
Split view classes by resource and responsibility.
Collection endpoint:
GET /api/v1/organizations/<organization_id>/companies/
POST /api/v1/organizations/<organization_id>/companies/Use one class:
class CompanyListCreateAPIView(APIView):
...Detail endpoint:
GET /api/v1/organizations/<organization_id>/
DELETE /api/v1/organizations/<organization_id>/Use another class:
class OrganizationDetailAPIView(APIView):
...Custom action endpoint:
POST /api/v1/organizations/<organization_id>/automations/<id>/test/Use another class:
class AutomationTestAPIView(APIView):
...Rule of thumb:
same path + same resource level -> same class, different methods
different path or custom action -> different classAutomation Response Semantics
Creating an automation saves a rule; it does not execute it. The manual test
endpoint executes the action and returns HTTP 201 with a run, including when that
run has status: failed. Clients must inspect the body's status and
run.error_message. This test is not a dry run and bypasses the active-rule
filter used by normal event dispatch.
There is no automation PATCH/DELETE or multi-step definition endpoint yet. See current automation behavior and the walkthrough. Future workflow definitions in the roadmap are not a current API contract.
Collection Search
For simple search, filter the queryset before creating the serializer.
Example:
from django.db.models import Q
def get(self, request, organization_id):
organization = get_organization_for_user(
user=request.user,
organization_id=organization_id,
capability=Capability.VIEW_CRM,
)
companies = Company.objects.filter(organization=organization)
search = request.query_params.get("search")
if search:
companies = companies.filter(
Q(name__icontains=search)
| Q(email__icontains=search)
| Q(phone_number__icontains=search)
| Q(website__icontains=search)
)
serializer = CompanySerializer(companies, many=True)
return Response(serializer.data)Important details:
- Filter before serializing.
- Use
Q(...) | Q(...)for OR search. - Multiple keyword arguments in
.filter(...)behave like AND.
Naming Convention
Recommended API class names:
OrganizationListCreateAPIView
OrganizationDetailAPIView
CompanyListCreateAPIView
CompanyDetailAPIView
ContactListCreateAPIView
ContactDetailAPIView
AutomationListCreateAPIView
AutomationRunListAPIView
AutomationTestAPIViewDetail is singular because the endpoint represents one resource.
REST Authentication Guide
This guide explains the JWT API contract independently from the React client. It is useful for Postman, curl, mobile clients, scripts, and contributors debugging a browser login.
Bubllio CRM API and Postman Guide
This guide explains how to exercise the authenticated, organization-scoped API with the files in docs/postman/.
