What is Documentation-Driven Development?
Documentation-Driven Development (DDD) is a software development methodology where you write the documentation first — before writing any implementation code. This documentation serves as the definitive specification, user guide, and design blueprint for your software. The core principle is simple: if you can't explain it clearly, you don't understand it well enough to build it.
The approach inverts the traditional "code first, document later" (or worse, "code first, document never") workflow. Instead, DDD treats documentation as the primary artifact from which all other development activities flow. The documentation becomes a living contract that drives API design, test cases, type definitions, and even the user interface.
The Core Philosophy
At its heart, DDD rests on three pillars:
- Clarity precedes code — Ambiguities in your thinking become painfully obvious when you try to articulate them in prose.
- Documentation is the source of truth — Not the code, not the whiteboard sketch, but the written specification that everyone can read and agree upon.
- Iteration happens on paper first — Changing prose is orders of magnitude cheaper than rewriting code. You iterate the design until it's solid, then implement.
Why Documentation-Driven Development Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →DDD isn't just about having nice docs. It fundamentally changes how teams collaborate and how software gets built. Here's why it matters:
1. Exposes Ambiguity Early
When you write documentation first, vague requirements become concrete. A statement like "the system should handle user authentication" forces you to decide: What authentication method? OAuth? JWT? Session-based? What happens on failure? Writing the docs forces these decisions before a single line of code is written.
2. Creates a Single Source of Truth
Without DDD, knowledge is scattered across Jira tickets, Slack threads, whiteboard photos, and the fading memories of senior engineers who have left the company. With DDD, the documentation is the canonical reference that everyone — developers, testers, product managers, and new hires — can consult.
3. Drives Better API Design
APIs designed from the documentation outward tend to be more consistent, more intuitive, and more pleasant to use. When you write the docs first, you experience the API as your users will. Painful or confusing interfaces become obvious immediately.
4. Makes Testing Trivial
Well-written documentation includes expected inputs, outputs, error conditions, and edge cases. These directly translate into test cases. In many DDD workflows, the documentation literally generates test stubs automatically.
5. Enables Parallel Work
Once the documentation is written and approved, frontend and backend teams can work simultaneously. Frontend developers can mock APIs based on the documented contracts. QA engineers can write test plans. Technical writers can refine the user-facing docs. Everyone moves forward in parallel.
6. Prevents "Silent Knowledge" Loss
When a developer leaves, their undocumented assumptions leave with them. DDD ensures that critical design decisions, trade-offs, and rationale are preserved in writing for future maintainers.
How to Use Documentation-Driven Development
Here is a practical, step-by-step workflow for adopting DDD in your projects:
Step 1: Start with the User-Facing Documentation
Begin by writing the documentation that your end user would read. This might be README sections, API reference pages, or a quick-start guide. Describe what the feature does, why it exists, and how someone would use it — all from the user's perspective.
# Example: Starting with user-facing docs for a notification service
# Notification Service
## Overview
The Notification Service allows you to send transactional notifications
to users via email, SMS, and push notifications.
## Quick Start
javascript
const { NotificationClient } = require('@acme/notifications');
const client = new NotificationClient({
apiKey: process.env.NOTIFICATION_API_KEY,
region: 'us-east-1'
});
await client.send({
channel: 'email',
to: 'user@example.com',
template: 'order-confirmation',
data: {
orderId: 'ORD-12345',
total: 99.95
}
});
## Supported Channels
- **email** — HTML and plain text email delivery
- **sms** — SMS delivery to US and international numbers
- **push** — Mobile push notifications via FCM and APNs
## Error Handling
All methods throw a `NotificationError` with properties:
- `code` — Machine-readable error code (e.g., `invalid_recipient`)
- `message` — Human-readable description
- `statusCode` — HTTP status code equivalent
Step 2: Write the Technical Specification
Next, expand the user-facing docs into a detailed technical specification. Define every endpoint, every function signature, every data type, every error code, and every edge case. This is your contract.
# Notification Service — Technical Specification
## API Endpoints
### POST /v1/notifications
Send a notification to one or more recipients.
**Request Body:**
json
{
"channel": "email | sms | push",
"recipients": [
{
"address": "user@example.com | +15551234567 | device-token-abc",
"preferences": {
"locale": "en-US",
"timezone": "America/New_York"
}
}
],
"template_id": "order-confirmation",
"template_data": {
"orderId": "string (required)",
"total": "number (required)",
"currency": "string (default: 'USD')"
},
"idempotency_key": "string (optional, max 255 chars)"
}
**Success Response (202 Accepted):**
json
{
"notification_id": "notif_abc123",
"status": "queued",
"recipient_count": 1,
"created_at": "2024-01-15T14:30:00Z"
}
**Error Responses:**
| Status Code | Error Code | Description |
|-------------|-------------------------|--------------------------------------|
| 400 | invalid_channel | Channel not supported |
| 400 | invalid_recipient | Recipient address malformed |
| 400 | template_not_found | Template ID does not exist |
| 409 | idempotency_conflict | Duplicate idempotency key detected |
| 429 | rate_limited | Too many requests, retry after delay |
| 500 | internal_error | Unexpected server error |
**Idempotency:**
Requests with the same `idempotency_key` within 24 hours return the
original response. This prevents duplicate notifications.
Step 3: Generate Code Scaffolding from Documentation
Modern DDD workflows often use tools that parse your documentation and generate code stubs, type definitions, and validation logic automatically. Here's an example using a fictional tool called docgen that reads OpenAPI specs (which you wrote as documentation) and generates TypeScript types:
# Your documentation (OpenAPI YAML) becomes the source of truth
# File: notification-service.openapi.yaml
openapi: 3.0.0
info:
title: Notification Service
version: 1.0.0
paths:
/v1/notifications:
post:
operationId: sendNotification
requestBody:
content:
application/json:
schema:
type: object
required: [channel, recipients, template_id, template_data]
properties:
channel:
type: string
enum: [email, sms, push]
recipients:
type: array
items:
type: object
required: [address]
properties:
address:
type: string
preferences:
type: object
properties:
locale:
type: string
timezone:
type: string
template_id:
type: string
template_data:
type: object
idempotency_key:
type: string
maxLength: 255
responses:
'202':
description: Notification queued successfully
content:
application/json:
schema:
type: object
properties:
notification_id:
type: string
status:
type: string
enum: [queued]
recipient_count:
type: integer
created_at:
type: string
format: date-time
Then run a generator to produce TypeScript types and validation:
# Command to generate code from the OpenAPI spec
$ npx openapi-generator-cli generate \
-i notification-service.openapi.yaml \
-g typescript-axios \
-o ./generated-client
# Generated output (example excerpt):
# File: generated-client/api.ts
export interface SendNotificationRequest {
channel: 'email' | 'sms' | 'push';
recipients: Array<{
address: string;
preferences?: {
locale?: string;
timezone?: string;
};
}>;
template_id: string;
template_data: Record;
idempotency_key?: string;
}
export interface SendNotificationResponse {
notification_id: string;
status: 'queued';
recipient_count: number;
created_at: string; // ISO 8601 date-time
}
// The generated client also includes request validation,
// error handling, and axios configuration — all from your docs.
Step 4: Write Tests Based on Documentation
Your documentation already specifies inputs, outputs, and error conditions. Translate these directly into test cases. This is where DDD truly shines — your test suite becomes a direct reflection of your documented contract.
// File: notification-service.test.ts
// Tests derived directly from the technical specification
import { NotificationClient, NotificationError } from './notification-client';
describe('POST /v1/notifications', () => {
const client = new NotificationClient({
apiKey: 'test-key',
baseUrl: 'http://localhost:9001'
});
// Test case from docs: Success Response (202 Accepted)
it('returns 202 with notification_id on successful send', async () => {
const response = await client.send({
channel: 'email',
recipients: [{ address: 'user@example.com' }],
template_id: 'order-confirmation',
template_data: { orderId: 'ORD-12345', total: 99.95 }
});
expect(response.status).toBe(202);
expect(response.data).toMatchObject({
notification_id: expect.stringMatching(/^notif_/),
status: 'queued',
recipient_count: 1,
created_at: expect.any(String)
});
});
// Test case from docs: 400 — invalid_channel
it('rejects invalid channels with error code invalid_channel', async () => {
try {
await client.send({
channel: 'fax', // not in the supported enum
recipients: [{ address: 'user@example.com' }],
template_id: 'order-confirmation',
template_data: { orderId: 'ORD-12345', total: 99.95 }
});
fail('Expected error was not thrown');
} catch (error) {
expect(error).toBeInstanceOf(NotificationError);
expect(error.code).toBe('invalid_channel');
expect(error.statusCode).toBe(400);
}
});
// Test case from docs: 409 — idempotency_conflict
it('returns original response for duplicate idempotency keys', async () => {
const idempotencyKey = 'idem-2024-01-15-001';
const firstResponse = await client.send({
channel: 'email',
recipients: [{ address: 'user@example.com' }],
template_id: 'order-confirmation',
template_data: { orderId: 'ORD-12345', total: 99.95 },
idempotency_key: idempotencyKey
});
// Second call with same key within 24 hours
const secondResponse = await client.send({
channel: 'email',
recipients: [{ address: 'user@example.com' }],
template_id: 'order-confirmation',
template_data: { orderId: 'ORD-12345', total: 99.95 },
idempotency_key: idempotencyKey
});
expect(secondResponse.status).toBe(409);
// Docs say: returns the original response
expect(secondResponse.data.notification_id)
.toBe(firstResponse.data.notification_id);
});
// Test case from docs: 400 — template_not_found
it('returns template_not_found for nonexistent templates', async () => {
try {
await client.send({
channel: 'email',
recipients: [{ address: 'user@example.com' }],
template_id: 'nonexistent-template-xyz',
template_data: { orderId: 'ORD-12345', total: 99.95 }
});
fail('Expected error was not thrown');
} catch (error) {
expect(error).toBeInstanceOf(NotificationError);
expect(error.code).toBe('template_not_found');
expect(error.statusCode).toBe(400);
}
});
// Edge case documented: empty recipients array
it('handles empty recipients array gracefully', async () => {
try {
await client.send({
channel: 'email',
recipients: [],
template_id: 'order-confirmation',
template_data: { orderId: 'ORD-12345', total: 99.95 }
});
fail('Expected error was not thrown');
} catch (error) {
expect(error.code).toBe('invalid_recipient');
}
});
});
Step 5: Implement to Make Tests Pass
Only now do you write the implementation code. You have a clear specification, generated types, and a comprehensive test suite. Your job is simply to make the tests pass while adhering to the documented contract.
// File: notification-client.ts
// Implementation driven by the documentation and tests
import axios, { AxiosInstance, AxiosError } from 'axios';
// Types imported from generated code (which came from docs)
interface SendNotificationRequest {
channel: 'email' | 'sms' | 'push';
recipients: Array<{
address: string;
preferences?: { locale?: string; timezone?: string };
}>;
template_id: string;
template_data: Record;
idempotency_key?: string;
}
interface SendNotificationResponse {
notification_id: string;
status: 'queued';
recipient_count: number;
created_at: string;
}
export class NotificationError extends Error {
public readonly code: string;
public readonly statusCode: number;
constructor(code: string, message: string, statusCode: number) {
super(message);
this.name = 'NotificationError';
this.code = code;
this.statusCode = statusCode;
}
}
export class NotificationClient {
private client: AxiosInstance;
constructor(config: { apiKey: string; baseUrl: string }) {
this.client = axios.create({
baseURL: config.baseUrl,
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json'
}
});
}
async send(request: SendNotificationRequest): Promise<{
status: number;
data: SendNotificationResponse;
}> {
try {
const response = await this.client.post('/v1/notifications', request);
return {
status: response.status,
data: response.data
};
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<{
error_code: string;
message: string;
}>;
if (axiosError.response) {
throw new NotificationError(
axiosError.response.data.error_code || 'unknown_error',
axiosError.response.data.message || 'An error occurred',
axiosError.response.status
);
}
}
throw new NotificationError(
'internal_error',
'Unexpected server error',
500
);
}
}
}
Step 6: Keep Documentation and Code in Sync
The final, ongoing step is maintenance. Whenever the API changes, you update the documentation first, then regenerate types, update tests, and finally modify the implementation. The documentation always leads. This ensures the contract never drifts from reality.
Best Practices for Documentation-Driven Development
1. Use Structured Documentation Formats
Plain Markdown is fine for high-level docs, but for technical specifications, prefer structured formats like OpenAPI (for REST APIs), AsyncAPI (for event-driven systems), or Protocol Buffers (for gRPC). These formats are machine-readable and can generate code, tests, and interactive documentation automatically.
# Good: OpenAPI spec (machine-readable, generates code)
openapi: 3.0.0
paths:
/users/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
type: string
pattern: '^[a-f0-9]{32}$'
# Avoid: Freeform Markdown that requires manual interpretation
# "GET /users/{id} — pass the user ID in the URL path.
# The ID should be a 32-character hex string probably."
2. Document Error States Exhaustively
One of the most common documentation gaps is error handling. Document every possible error condition, including the exact error code, HTTP status (if applicable), and a human-readable message. This is invaluable for client developers and directly feeds your test suite.
3. Include Real-World Examples for Every Operation
Every endpoint, every function, every method should have at least one complete, copy-pasteable example that actually works. Examples serve as both documentation and implicit tests. Users should be able to take your example, substitute their own data, and get a working result.
# Good documentation with runnable examples
## POST /v1/orders — Create a new order
**Example Request:**
bash
curl -X POST https://api.example.com/v1/orders \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"sku": "SHIRT-BLUE-M", "quantity": 2},
{"sku": "PANTS-BLACK-34", "quantity": 1}
],
"shipping_address": {
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
}
}'
**Example Response (201 Created):**
json
{
"order_id": "ord_abc123def456",
"status": "confirmed",
"total_amount": 89.97,
"estimated_delivery": "2024-01-22"
}
4. Version Your Documentation Alongside Your Code
Store documentation in the same repository as your code, under version control. Use semantic versioning for your API, and keep documentation versions aligned with code releases. A v1 API should have v1 documentation committed alongside the v1 implementation.
# Example repository structure
project-root/
├── docs/
│ ├── v1/
│ │ └── openapi.yaml # v1 API specification
│ ├── v2/
│ │ └── openapi.yaml # v2 API specification (in development)
│ └── architecture/
│ └── decision-records/ # ADRs documenting key decisions
├── src/
│ ├── v1/
│ │ └── handlers/ # v1 implementation
│ └── v2/
│ └── handlers/ # v2 implementation
└── tests/
├── v1/
│ └── contract-tests/ # Tests generated from v1 docs
└── v2/
└── contract-tests/ # Tests generated from v2 docs
5. Adopt the "Documentation Review" as a Gate
In a DDD workflow, the documentation review is a formal step that must be completed before implementation begins. This review should include stakeholders from engineering, product, QA, and — critically — potential consumers of the API. No code is written until the docs are approved.
6. Write Documentation That Fails Gracefully
Good documentation anticipates common mistakes. Include sections like "Common Pitfalls" or "Frequently Encountered Errors" that explain what went wrong and how to fix it. This reduces support burden and improves developer experience.
# Example: Documentation that anticipates mistakes
## Common Pitfalls
### "Why am I getting a 403 Forbidden?"
Check that your API key has the correct permissions. The Notification
Service requires the `notifications:send` scope. You can verify your
key's scopes at https://dashboard.example.com/api-keys.
### "My idempotency key isn't working"
Idempotency keys are scoped to 24 hours. After 24 hours, a reused key
will create a new notification rather than returning the original.
Also ensure your key is unique — if another request used the same key
within the 24-hour window, you'll receive their response.
7. Use Documentation to Drive Code Reviews
During code review, always reference the documentation. If the implementation deviates from the documented contract, either the code is wrong or the docs need updating — and in DDD, the docs should be updated first. The reviewer's primary question should be: "Does this code faithfully implement the documented specification?"
8. Automate the Feedback Loop
Set up CI/CD pipelines that validate documentation, generate code from docs, run contract tests, and flag any drift between documentation and implementation. For OpenAPI specs, tools like Spectral can lint your spec, and Dredd can validate that your implementation matches the spec.
# Example CI pipeline step: validate docs and test contract compliance
# .github/workflows/contract-tests.yml
jobs:
contract-validation:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Lint OpenAPI spec
run: npx @stoplight/spectral-cli lint docs/openapi.yaml
- name: Start service
run: docker-compose up -d notification-service
- name: Run contract tests (Dredd)
run: |
npx dredd docs/openapi.yaml http://localhost:9001 \
--hookfiles=tests/hooks/*.js \
--fail-on-warnings
- name: Check for undocumented endpoints
run: |
# Compare registered routes against documented paths
npx api-drift-checker \
--spec docs/openapi.yaml \
--routes src/routes.ts \
--fail-on-missing-docs
Real-World Example: Building a Feature End-to-End with DDD
Let's walk through building a "User Preferences" feature using the full DDD workflow. This demonstrates how documentation leads every step.
Phase 1: User-Facing Documentation
# User Preferences API — Initial Draft
## Overview
Manage per-user preferences including notification settings,
display preferences, and locale configuration.
## Endpoints
### GET /users/:userId/preferences
Retrieve all preferences for a user.
### PATCH /users/:userId/preferences
Update one or more preferences. Only the fields you include
will be updated; omitted fields retain their current values.
## Preference Object
json
{
"notifications": {
"email_enabled": true,
"sms_enabled": false,
"push_enabled": true,
"digest_frequency": "daily | weekly | never"
},
"display": {
"theme": "light | dark | system",
"font_size": "small | medium | large",
"high_contrast": false
},
"locale": "en-US",
"timezone": "America/Chicago"
}
Phase 2: Detailed Technical Specification
# User Preferences — Technical Specification
# Written before any implementation code
## GET /users/:userId/preferences
**Path Parameter:**
- `userId` (string, required, pattern: `^usr_[a-f0-9]{24}$`)
**Success Response (200):**
json
{
"user_id": "usr_abc123def456789012345678",
"preferences": {
"notifications": {
"email_enabled": true,
"sms_enabled": false,
"push_enabled": true,
"digest_frequency": "daily"
},
"display": {
"theme": "system",
"font_size": "medium",
"high_contrast": false
},
"locale": "en-US",
"timezone": "America/Chicago"
},
"updated_at": "2024-01-15T14:30:00Z"
}
**Error: User Not Found (404):**
json
{
"error": {
"code": "user_not_found",
"message": "No user found with the specified ID",
"details": {
"user_id": "usr_nonexistent1234567890"
}
}
}
## PATCH /users/:userId/preferences
**Request Body (JSON):**
All fields are optional. Only include the fields you want to change.
json
{
"notifications": {
"email_enabled": true,
"sms_enabled": false,
"push_enabled": true,
"digest_frequency": "daily"
},
"display": {
"theme": "system",
"font_size": "medium",
"high_contrast": false
},
"locale": "en-US",
"timezone": "America/Chicago"
}
**Validation Rules:**
- `notifications.digest_frequency`: Must be one of `daily`, `weekly`, `never`
- `display.theme`: Must be one of `light`, `dark`, `system`
- `display.font_size`: Must be one of `small`, `medium`, `large`
- `locale`: Must be a valid BCP 47 language tag (e.g., `en-US`, `fr-CA`)
- `timezone`: Must be a valid IANA timezone string
**Success Response (200):**
Returns the complete updated preferences object (same shape as GET).
**Error: Validation Failure (400):**
json
{
"error": {
"code": "validation_error",
"message": "One or more fields failed validation",
"details": {
"fields": [
{
"path": "notifications.digest_frequency",
"message": "Must be one of: daily, weekly, never",
"received": "hourly"
}
]
}
}
}
Phase 3: Generate Types and Write Tests
// Generated TypeScript types from the spec
// File: generated/types.ts
export interface NotificationPreferences {
email_enabled: boolean;
sms_enabled: boolean;
push_enabled: boolean;
digest_frequency: 'daily' | 'weekly' | 'never';
}
export interface DisplayPreferences {
theme: 'light' | 'dark' | 'system';
font_size: 'small' | 'medium' | 'large';
high_contrast: boolean;
}
export interface UserPreferences {
notifications: NotificationPreferences;
display: DisplayPreferences;
locale: string; // BCP 47 language tag
timezone: string; // IANA timezone
}
export interface PatchPreferencesRequest {
notifications?: Partial;
display?: Partial;
locale?: string;
timezone?: string;
}
export interface PreferencesResponse {
user_id: string;
preferences: UserPreferences;
updated_at: string; // ISO 8601
}
// Test file: preferences-contract.test.ts
// Written BEFORE implementation, based entirely on docs
describe('User Preferences API — Contract Tests', () => {
describe('GET /users/:userId/preferences', () => {
it('returns 200 with full preferences for valid user', async () => {
const response = await api.get('/users/usr_validuser1234567890ab/preferences');
expect(response.status).toBe(200);
expect(response.body.preferences).toHaveProperty('notifications');
expect(response.body.preferences).toHaveProperty('display');
expect(response.body.preferences).toHaveProperty('locale');
expect(response.body.preferences).toHaveProperty('timezone');
expect(response.body.user_id).toBe('usr_validuser1234567890ab');
});
it('returns 404 for nonexistent user', async () => {
const response = await api.get('/users/usr_nonexistent1234567890/preferences');
expect(response.status).toBe(404);
expect(response.body.error.code).toBe('user_not_found');
});
});
describe('PATCH /users/:userId/preferences', () => {
it('partially updates preferences — only changed fields', async () => {
const response = await api.patch('/users/usr_validuser1234567890ab/preferences')
.send({ display: { theme: 'dark' } });
expect(response.status).toBe(200);
expect(response.body.preferences.display.theme).toBe('dark');
// Unchanged fields should retain their values
expect(response.body.preferences.display.font_size).toBeDefined();
});
it('returns 400 for invalid digest_frequency value', async () => {
const response = await api.patch('/users/usr_validuser1234567890ab/preferences')
.send({ notifications: { digest_frequency: 'hourly' } });
expect(response.status).toBe(400);
expect(response.body.error.code).toBe('validation_error');
expect(response.body.error.details.fields[0].path)
.toBe('notifications.digest_frequency');
});
it('returns 400 for invalid locale format', async () => {
const response = await api.patch('/users/usr_validuser1234567890ab/preferences')
.send({ locale: 'not-a-locale!!!' });
expect(response.status).toBe(400);
});
});
});
Phase 4: Implement the Handler
// File: preferences-handler.ts
// Implementation written to satisfy the documented contract and tests
import { Request, Response, NextFunction } from 'express';
import { validate } from 'class-validator';
import { UserPreferences, PatchPreferencesRequest } from './generated/types';
const VALID_DIGEST_FREQUENCIES = ['daily', 'weekly', 'never'];
const VALID_THEMES = ['light', 'dark', 'system'];
const VALID_FONT_SIZES = ['small', 'medium', 'large'];
const BCP47_REGEX = /^[a-z]{2,3}(-[A-Z]{2,3})?(-[A-Za-z0-9]+)*$/;
const IANA_TIMEZONE_REGEX = /^[A-Za-z]+\/[A-Za-z_]+$/;
function validatePreferences(body: PatchPreferencesRequest): string[] {
const errors: string[] = [];
if (body.notifications?.digest_frequency &&
!VALID_DIGEST_FREQUENCIES.includes(body.notifications.digest_frequency)) {
errors.push(`notifications.digest_frequency: Must be one of daily, weekly, never`);
}
if (body.display?.theme && !VALID_THEMES.includes(body.display.theme)) {
errors.push(`display.theme: Must be one of light, dark, system`);
}
if (body.display?.font_size && !VALID_FONT_SIZES.includes(body.display.font_size)) {
errors.push(`display.font_size: Must be one of small, medium, large`);
}
if (body.locale && !BCP47_REGEX.test(body.locale)) {
errors.push(`locale: Must be a valid BCP 47 language tag`);
}
if (body.timezone && !IANA_TIMEZONE_REGEX.test(body.timezone)) {
errors.push(`timezone: Must be a valid IANA timezone string`);
}
return errors;
}
export async function getPreferences(req: Request, res: Response, next: NextFunction) {
const { userId } = req.params;
// Validate userId format per docs: pattern ^usr_[a-f0-9]{24}$
if (!/^usr_[a-f0-9]{24}$/.test(userId)) {
return res.status(400).json({
error: { code: 'invalid_user_id', message: 'User ID format is invalid' }
});
}
const preferences = await preferencesService.getByUserId(userId);
if (!preferences) {
return res.status(404).json({
error: {
code: 'user_not_found',
message: 'No user found with the specified ID',
details: { user_id: userId }
}
});
}
return res.status(200).json({
user_id: userId,
preferences,
updated_at: preferences.updatedAt.toISOString()
});
}
export async function patchPreferences(req: Request, res: Response, next: NextFunction) {
const { userId } = req.params;
const body: PatchPreferencesRequest = req.body;
// Validate request body against documented rules
const validationErrors = validatePreferences(body);
if (validationErrors.length > 0) {
return res.status(400).json({
error: {
code: 'validation_error',
message: 'One or more fields failed validation',
details: {
fields: validationErrors.map(e => {
const [path, message] = e.split(': ');
return { path: path.trim(), message };
})
}
}
});
}
// Merge with existing preferences (PATCH semantics per docs)
const updatedPreferences = await preferencesService.patch(userId, body);
if (!updatedPreferences) {
return res.status(404).json({
error: {
code: 'user_not_found',
message: 'No user found with the specified ID',
details: { user_id: userId }
}
});
}
return res.status(200).json({
user_id: userId,
preferences: updatedPreferences,
updated_at: new Date().toISOString()
});
}
Common Anti-Patterns to Avoid
- "We'll document it later" — This is the antithesis of DDD. Documentation deferred is documentation never written, or documentation that's inaccurate the moment it's finally produced.
- Documentation that merely describes what code does — Good documentation specifies intent and contract