This guide demonstrates how to build a new feature end-to-end in CARE's backend, using the Patient model as a concrete example. Follow this guide to understand how to create new models and APIs that integrate seamlessly with CARE's architecture.
Our backend uses:
- Django & Django REST Framework: For models, APIs, and business logic
- Pydantic: For API contract definitions and validation
- PostgreSQL: As the primary database
Key directories:
- Models:
care/emr/models/- ExtendEMRBaseModelfor audit fields and common behavior - Resources (Pydantic):
care/emr/resources/<feature>/spec.py- Define create/update/read contracts - API ViewSets:
care/emr/api/viewsets/- Compose mixins fromEMRBaseViewSet - Routing:
config/api_router.py- Use nested routers for facility/patient-scoped endpoints
Let's look at the Patient model as an example of best practices. The model is defined in care/emr/models/patient.py and extends EMRBaseModel.
- Field Definitions:
class Patient(EMRBaseModel):
name = models.CharField(max_length=200, default="")
gender = models.CharField(max_length=35, default="")
phone_number = models.CharField(
max_length=14,
validators=[mobile_or_landline_number_validator],
default=""
)
date_of_birth = models.DateField(default=None, null=True)
year_of_birth = models.IntegerField(
validators=[MinValueValidator(1900)],
null=True
)-
Best Practices:
- Extend
EMRBaseModelto get audit fields (created/modified dates, external_id) - Use appropriate field types and validators
- Keep normalized fields and caches separate
- Document complex fields or business logic
- Use descriptive field names
- Extend
-
Model Methods:
- Keep
save()methods minimal and safe - Encapsulate complex logic in helper methods
- Use properties for derived fields
- Handle caches and signals appropriately
- Keep
Resources define the API contracts and handle model mapping. They live in care/emr/resources/<feature>/spec.py.
- Base Spec:
class PatientBaseSpec(EMRResource):
name: str
gender: str
phone_number: str
date_of_birth: datetime.date | None = None
year_of_birth: int | None = None- Create Spec:
class PatientCreateSpec(PatientBaseSpec):
geo_organization: UUID4
identifiers: list[PatientIdentifierConfigRequest] = []
tags: list[UUID4] = []
@model_validator(mode="after")
def validate_identifiers(self):
# Validation logic here
return self
def perform_extra_deserialization(self, is_update, obj):
# Map Pydantic fields to Django model
obj.geo_organization = Organization.objects.get(
external_id=self.geo_organization
)- Update/List/Retrieve Specs:
- Create separate specs for different operations
- Handle partial updates in UpdateSpec
- Add computed fields in ListSpec/RetrieveSpec
- Use validators for complex validation logic
- Handle related objects in
perform_extra_deserialization - Keep serialization logic in
perform_extra_serialization - Document any non-obvious mappings
ViewSets compose EMR mixins and handle authorization. They live in care/emr/api/viewsets/.
class PatientViewSet(EMRModelViewSet):
database_model = Patient
pydantic_model = PatientCreateSpec
pydantic_read_model = PatientListSpec
pydantic_update_model = PatientUpdateSpec
pydantic_retrieve_model = PatientRetrieveSpec
filterset_class = PatientFilters
ordering_fields = ["created_date", "modified_date"]- Authorization:
def authorize_update(self, request_obj, model_instance):
if not AuthorizationController.call(
"can_write_patient_obj",
self.request.user,
model_instance
):
raise PermissionDenied("Cannot Update Patient")- Create/Update Operations:
def perform_create(self, instance):
with transaction.atomic():
super().perform_create(instance)
# Additional setup like tags, identifiers
instance.save()- Use transactions for multi-step operations
- Keep business logic in model methods
- Implement proper authorization checks
- Add appropriate filters and ordering
- Optimize querysets with select_related/prefetch_related
Register your ViewSet in config/api_router.py:
# For top-level resources
router.register("patient", PatientViewSet, basename="patient")
# For nested resources under patient
patient_nested_router = NestedSimpleRouter(
router, r"patient", lookup="patient"
)
patient_nested_router.register(
r"consultation",
ConsultationViewSet,
basename="patient-consultation"
)Choose the appropriate router based on your resource's scope:
- Root router: For top-level resources
facility_nested_router: For facility-scoped resourcespatient_nested_router: For patient-scoped resources
Create comprehensive tests covering all operations:
@pytest.mark.django_db
class TestPatientAPI:
def test_create_patient(self, user_factory):
client = APIClient()
user = user_factory()
client.force_authenticate(user)
url = reverse("api:patient-list")
payload = {
"name": "John Doe",
"gender": "male",
"phone_number": "+910000000000",
"geo_organization": "<org-external-id>",
}
res = client.post(url, payload, format="json")
assert res.status_code == 200
assert res.data["name"] == "John Doe"
def test_update_patient(self, patient_factory):
# Test update operations
pass
def test_list_filters(self):
# Test list filters and ordering
pass- Basic CRUD operations
- Validation error cases
- Authorization rules
- Custom actions/endpoints
- Edge cases and business rules
When creating a new feature:
-
Create model and migrations
- Extend EMRBaseModel
- Add appropriate fields and validators
- Document complex logic
-
Create Pydantic resources
- Base spec with common fields
- Create/Update/List/Retrieve specs
- Add validation rules
- Implement serialization methods
-
Implement ViewSet
- Set up basic configuration
- Add authorization rules
- Implement custom actions
- Add filters and ordering
-
Update routing
- Register with appropriate router
- Add nested routes if needed
-
Write tests
- CRUD operations
- Validation cases
- Authorization rules
- Custom functionality
-
Documentation
- API endpoints
- Request/response formats
- Authorization requirements
- Example usage
-
Authorization:
- Always implement proper authorization checks
- Consider both object and action-level permissions
- Test with different user roles
-
Transactions:
- Use atomic transactions for multi-step operations
- Handle rollbacks appropriately
- Consider using select_for_update when needed
-
Performance:
- Optimize database queries
- Use select_related/prefetch_related
- Add appropriate indexes
- Cache expensive computations
-
Validation:
- Add comprehensive validation rules
- Handle edge cases
- Provide clear error messages
- Validate at both model and API levels