Owner: Engineering Team | Last Updated: 2026-01-30 | Status: Current
This guide covers the end-to-end process of developing a new feature in the WWAI platform. It applies to changes that span the Django backend, Next.js web app, or both. Follow each phase in order; skipping steps leads to rework and delayed releases.
# apps/<app_name>/models.py
from django.db import models
class FeatureEntity(models.Model):
"""Brief description of the model purpose."""
name = models.CharField(max_length=255)
user = models.ForeignKey("users.User", on_delete=models.CASCADE, related_name="feature_entities")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
verbose_name_plural = "feature entities"
def __str__(self) -> str:
return self.name
python manage.py makemigrations and review the generated migration file.python manage.py migrate to apply locally.# apps/<app_name>/serializers.py
from rest_framework import serializers
from .models import FeatureEntity
class FeatureEntitySerializer(serializers.ModelSerializer):
class Meta:
model = FeatureEntity
fields = ["id", "name", "created_at", "updated_at"]
read_only_fields = ["id", "created_at", "updated_at"]
# apps/<app_name>/views.py
from rest_framework import viewsets, permissions
from .models import FeatureEntity
from .serializers import FeatureEntitySerializer
class FeatureEntityViewSet(viewsets.ModelViewSet):
serializer_class = FeatureEntitySerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return FeatureEntity.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
# apps/<app_name>/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import FeatureEntityViewSet
router = DefaultRouter()
router.register(r"feature-entities", FeatureEntityViewSet, basename="feature-entity")
urlpatterns = [
path("", include(router.urls)),
]
urls.py in the project-level urls.py if not already done.# apps/<app_name>/tests/test_feature_entity.py
from django.test import TestCase
from rest_framework.test import APIClient
from users.factories import UserFactory
class FeatureEntityAPITest(TestCase):
def setUp(self):
self.client = APIClient()
self.user = UserFactory()
self.client.force_authenticate(user=self.user)
def test_create_feature_entity(self):
response = self.client.post("/api/feature-entities/", {"name": "Test"})
self.assertEqual(response.status_code, 201)
def test_list_feature_entities(self):
response = self.client.get("/api/feature-entities/")
self.assertEqual(response.status_code, 200)
Important: The WWAI frontend has no
src/directory. All top-level directories (components/,hooks/,utils/,lib/,types/) live at the project root. The@/*path alias maps to the project root.
Add shared TypeScript interfaces to the central types file:
// types/index.ts
export interface FeatureEntity {
id: string
name: string
created_at: string
updated_at: string
}
The codebase uses two API patterns. Use the axios pattern (via utils/api.ts) for most endpoints. The humanizer feature uses native fetch() as a one-off inconsistency -- do not replicate that pattern for new features.
// utils/featureEntities.ts
import api from '@/utils/api'
export const getFeatureEntities = async (token: string) => {
const response = await api.get('/api/feature-entities/', {
headers: { Authorization: `Bearer ${token}` },
})
return response.data
}
export const createFeatureEntity = async (
token: string,
data: { name: string }
) => {
const response = await api.post('/api/feature-entities/', data, {
headers: { Authorization: `Bearer ${token}` },
})
return response.data
}
Note on fetch vs. axios: The humanizer feature (
components/partials/humanizer/Humanizer.tsx) usesfetch()withprocess.env.NEXT_PUBLIC_BACKEND_URLdirectly. All other features use theapiaxios instance from@/utils/api. Prefer the axios pattern for new work.
For features with complex state or data fetched from the backend, create a Redux Toolkit slice:
// lib/store/featureEntitiesSlice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import api from '@/utils/api'
interface FeatureEntity {
id: string
name: string
created_at: string
updated_at: string
}
interface FeatureEntitiesState {
data: FeatureEntity[] | null
loading: boolean
error: string | null | undefined
}
const initialState: FeatureEntitiesState = {
data: null,
loading: false,
error: null,
}
export const fetchFeatureEntities = createAsyncThunk(
'featureEntities/fetchFeatureEntities',
async ({ token }: { token: string }) => {
const response = await api.get('/api/feature-entities/', {
headers: { Authorization: `Bearer ${token}` },
})
return response.data
}
)
export const createFeatureEntity = createAsyncThunk(
'featureEntities/createFeatureEntity',
async ({ token, name }: { token: string; name: string }) => {
const response = await api.post(
'/api/feature-entities/',
{ name },
{ headers: { Authorization: `Bearer ${token}` } }
)
return response.data
}
)
const featureEntitiesSlice = createSlice({
name: 'featureEntities',
initialState,
reducers: {
clearFeatureEntities: (state) => {
state.data = null
state.error = null
},
},
extraReducers: (builder) => {
builder
.addCase(fetchFeatureEntities.pending, (state) => {
state.loading = true
state.error = null
})
.addCase(fetchFeatureEntities.fulfilled, (state, action) => {
state.data = action.payload
state.loading = false
})
.addCase(fetchFeatureEntities.rejected, (state, action) => {
state.error = action.error.message
state.loading = false
})
},
})
export const { clearFeatureEntities } = featureEntitiesSlice.actions
export default featureEntitiesSlice.reducer
Then register the slice in the store configuration.
Real example: See
lib/store/documentsSlice.tsfor a production Redux slice with async thunks following this exact pattern.
WWAI components use plain arrow functions with destructured typed props (no React.FC). The project enforces no semicolons and single quotes via Prettier.
// components/partials/feature-entities/FeatureEntityCard.tsx
'use client'
import { useTranslations } from 'next-intl'
interface FeatureEntityCardProps {
entity: {
id: string
name: string
created_at: string
}
onSelect?: (id: string) => void
}
const FeatureEntityCard = ({ entity, onSelect }: FeatureEntityCardProps) => {
const t = useTranslations('featureEntities')
return (
<div
className="rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow"
onClick={() => onSelect?.(entity.id)}
role="button"
tabIndex={0}
>
<h3 className="text-lg font-semibold">{entity.name}</h3>
<p className="text-sm text-gray-500">
{t('createdAt')}: {new Date(entity.created_at).toLocaleDateString()}
</p>
</div>
)
}
export default FeatureEntityCard
// app/[locale]/dashboard/feature-entities/page.tsx
'use client'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useTranslations } from 'next-intl'
import type { AppDispatch, RootState } from '@/lib/store'
import { fetchFeatureEntities } from '@/lib/store/featureEntitiesSlice'
import FeatureEntityCard from '@/components/partials/feature-entities/FeatureEntityCard'
const FeatureEntitiesPage = () => {
const t = useTranslations('featureEntities')
const dispatch = useDispatch<AppDispatch>()
const { data, loading, error } = useSelector(
(state: RootState) => state.featureEntities
)
const token = useSelector((state: RootState) => state.auth.token)
useEffect(() => {
if (token) {
dispatch(fetchFeatureEntities({ token }))
}
}, [dispatch, token])
if (loading) return <div>{t('loading')}</div>
if (error) return <div>{t('error')}</div>
return (
<div className="container mx-auto py-8">
<h1 className="text-2xl font-bold mb-6">{t('title')}</h1>
{data && data.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.map((entity) => (
<FeatureEntityCard key={entity.id} entity={entity} />
))}
</div>
) : (
<p className="text-gray-500">{t('emptyState')}</p>
)}
</div>
)
}
export default FeatureEntitiesPage
| Need | Approach |
|---|---|
| Server data shared across components | Redux Toolkit slice in lib/store/ with async thunks |
| Local UI state (toggles, form inputs) | useState / useReducer inside 'use client' components |
| Dispatch actions | useDispatch<AppDispatch>() directly (no custom hooks) |
| Read store state | useSelector((state: RootState) => state.sliceName) directly |
Important: The codebase uses
useDispatch<AppDispatch>()anduseSelector((state: RootState) => ...)directly -- there are no custom typed hooks likeuseAppDispatchoruseAppSelector.
Add keys to all 8 locale files under messages/:
// messages/en.json
{
"featureEntities": {
"title": "Feature Entities",
"createButton": "Create New",
"emptyState": "No entities found. Create your first one.",
"loading": "Loading...",
"error": "Something went wrong.",
"createdAt": "Created"
}
}
en.json, de.json, es.json, fr.json, it.json, pt.json, zh.json, nl.json.import { useTranslations } from 'next-intl'
const MyComponent = () => {
const t = useTranslations('featureEntities')
return <h1>{t('title')}</h1>
}
Rule: Never hardcode user-facing strings. Every visible string must go through
useTranslationsfromnext-intl.
The WWAI frontend does not have an automated test suite at this time. All testing is manual. Follow this checklist before submitting your PR.
Functional Testing:
Cross-Browser Testing:
Responsive Design:
Integration Points:
Backend Testing:
python manage.py test apps/<app_name>/.feature/TICKET-123-short-description
bugfix/TICKET-456-fix-entity-creation
hotfix/TICKET-789-critical-auth-fix
feat(feature-entities): add CRUD API and frontend page
- Create Django model, serializer, and viewset
- Add Next.js page with Redux slice and components
- Add i18n keys for all 8 locales
Refs: TICKET-123
Prefix conventions: feat, fix, refactor, docs, test, chore, perf.
feature branch
--> PR merged to dev
--> GitHub Actions: lint + type-check + build
--> Deploy to staging (ECS)
--> QA sign-off
--> PR from dev to main
--> Deploy to production (ECS)
Study these existing features to understand real patterns in the codebase:
components/partials/humanizer/Humanizer.tsx
fetch() directly (exception to the axios pattern)components/partials/humanizer/DetectorButton.tsx
lib/store/documentsSlice.ts
createAsyncThunk with token-based auth, createSlice with extraReducers buildercomponents/partials/settings/
| Rule | Value | Enforced By |
|---|---|---|
| Semicolons | None | Prettier (semi: false) |
| Quotes | Single | Prettier (singleQuote: true) |
| Component style | Arrow function, export default |
Convention |
| Props typing | Destructured with inline interface | Convention |
| Path alias | @/* maps to project root |
tsconfig.json |
| API client | import api from '@/utils/api' |
Convention |
| i18n | useTranslations from next-intl |
Convention |
| State management | Redux Toolkit in lib/store/ |
Convention |
| Store hooks | useDispatch<AppDispatch>(), useSelector((state: RootState) => ...) |
Convention |
| Date | Author | Change |
|---|---|---|
| 2026-01-30 | Admin | Updated frontend examples to match actual codebase patterns, added reference implementations, fixed import paths and code style |
| 2026-01-30 | Admin | Initial creation |
Next: Adding a New Frontend Client | Up: General