# Django Drf Layered Architecture

> Implements and reviews Django + DRF backends using strict layered decoupling (views, serializers, services, DTOs, models, adapters), versioned /api/v1/ routes, and thin HTTP boundaries. Use when scaffolding Django apps, adding endpoints, writing services/DTOs, refactoring fat views, or starting projects like OrbitQA that follow Lumi-style architecture (multi-tenant optional).

- Skill: `omayka-tech/django-drf-layered-architecture` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add omayka-tech/django-drf-layered-architecture`
- Raw SKILL.md: https://api.skillmd.com/api/skills/omayka-tech/django-drf-layered-architecture/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: Omayka-Tech (https://skillmd.com/u/omayka-tech)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/omayka-tech/django-drf-layered-architecture

---


# Django + DRF layered architecture

Portable rules distilled from Lumi ERP / Omayka backends. Apply to **new and existing** Django+DRF projects unless the repo explicitly overrides them.

## Layer dependency (strict)

```text
HTTP → views (DRF) → serializers (I/O edge)
                  → services (use cases)
                  → dtos (typed contracts)
                  → models (ORM)
                  → adapters/* (external I/O: HTTP, browser, LLM, files)
```

| Layer | Owns | Must not |
|-------|------|----------|
| **views** | Auth, status codes, call service, return Response | Business rules, ORM queries beyond get_queryset helpers, Playwright/LLM |
| **serializers** | Validate request/response; map ↔ DTO | Domain rules, calling other modules' models for decisions |
| **dtos** | Dataclass / TypedDict contracts in/out of services | Touch ORM or request |
| **services** | Transactions, validation, orchestration, emit events | Know DRF request; scrape UI; fat loops in serializers |
| **models** | Persistence, constraints, indexes | Decide multi-step workflows |
| **adapters** | External systems | Import views; leak HTTP details into services beyond DTOs |

**Services receive and return DTOs**, not `request` and ideally not raw models at the public method boundary (map model → DTO inside the service).

A service may call another service in the same app. **Never** import views from services. Cross-app: prefer DTOs, service APIs, or domain events — avoid importing another app's models for writes when a service exists.

## API versioning

- Expose new endpoints only under **`/api/v1/...`** (or the project's current version prefix).
- Do not add new clients to unversioned `/api/...` routes.

## App layout (per module)

```text
apps/<module>/
  models.py
  dtos.py
  services.py
  serializers.py
  views.py
  urls.py
  apps.py
  tests/
adapters/                 # optional package at backend root
  <system>/               # e.g. lumi/, llm/, payments/
```

Shared bits: `apps/common/` (`BaseDTO`, exceptions, EventBus helpers).

## DTO patterns

```python
from dataclasses import dataclass
from apps.common.dtos import BaseDTO

@dataclass
class CreateAgentDTO(BaseDTO):
    name: str
    role: str

@dataclass
class AgentDTO(BaseDTO):
    id: str
    name: str
    role: str
```

- Serializers validate input → build DTO → `Service().method(dto)` → serialize DTO/dict out.
- Type-hint service public methods.

## View pattern (thin)

```python
class AgentViewSet(viewsets.ViewSet):
    def create(self, request):
        ser = CreateAgentSerializer(data=request.data)
        ser.is_valid(raise_exception=True)
        dto = CreateAgentDTO(**ser.validated_data)
        result = AgentService().create(dto)
        return Response(AgentSerializer(result).data, status=201)
```

## Service pattern

```python
class AgentService:
    @transaction.atomic
    def create(self, dto: CreateAgentDTO) -> AgentDTO:
        # validate + persist + map to AgentDTO
        ...
```

## Adapters (external edges)

- Browser automation, third-party HTTP, LLM, filesystem: live under `adapters/`.
- Services call adapters with DTOs; adapters return DTOs (`SkillResultDTO`, etc.).
- Keep selectors, SDK clients, and retries inside the adapter.

## Multi-tenancy (optional)

- If the project uses schema-per-tenant (`django-tenants`): never cross schemas; scope queries to active tenant.
- If **not** multi-tenant (e.g. OrbitQA MVP): still keep services/DTO boundaries; omit `tenant_id` from DTOs unless needed later.

## Models checklist

- Prefer UUID PKs on new business models.
- `created_at` / `updated_at` on business entities.
- Never delete old migrations; add new ones.
- List endpoints: `select_related` / `prefetch_related`; avoid per-row DB in `SerializerMethodField` (batch via `serializer context`).

## Tests

- Prefer pytest + pytest-django.
- Test **behaviors** via services (and API smoke), not private helpers only.
- Mock adapters / external I/O in unit tests.

## Anti-patterns

- Fat `perform_create` with business rules.
- Serializers that call `Model.objects.create` with domain branching.
- Services that import `rest_framework` request objects.
- Views that launch Playwright or call OpenAI directly.
- Cross-app model imports for writes when a service exists.
- Unversioned new API routes.

## Checklist (new endpoint)

- [ ] URL under `/api/v1/`
- [ ] Serializer validates only
- [ ] DTO defined
- [ ] Service method with types + transaction if needed
- [ ] View thin
- [ ] External I/O behind adapter
- [ ] Tests for happy path + main validation error

## More

- Concrete snippets: [examples.md](examples.md)

