AI Returns Database Rows as API Responses
AI generates APIs that return: raw database rows (including internal IDs, timestamps in database format, and soft-delete flags), inconsistent date formats (ISO string in one endpoint, Unix timestamp in another, localized string in a third), snake_case from the database mixed with camelCase from the code, internal fields (password hashes, internal notes, admin flags), and no pagination envelope (array of objects with no total count, page info, or links).
Modern data serialization is: shaped (DTOs transform internal models to API contracts), consistent (camelCase everywhere, ISO 8601 dates, UTC timezone), filtered (never expose internal fields — whitelist, not blacklist), enveloped (pagination metadata wraps the data array), and versioned (serialization changes are version bumps, not surprise breaking changes). AI generates none of these.
These rules cover: DTO response shaping, date and timezone formatting, naming conventions, pagination envelopes, and field filtering strategies.
Rule 1: DTO Pattern for Response Shaping
The rule: 'Never return database models directly. Transform to DTOs (Data Transfer Objects) before sending. The DTO defines exactly what the client receives — no more, no less. function toUserDTO(user: UserModel): UserDTO { return { id: user.id, name: user.name, email: user.email, role: user.role, createdAt: user.createdAt.toISOString() }; }. The DTO is the API contract; the model is the database schema. They evolve independently.'
For why separation matters: 'Database models change when the schema changes (add column, rename column, change type). DTOs change when the API contract changes (new version). Without DTOs, every database migration is a potential API breaking change. With DTOs, you rename a database column and update the DTO mapper — the API response is unchanged. The DTO is a firewall between your database and your clients.'
AI generates: res.json(user) — the entire database row. password_hash, internal_notes, is_deleted, updated_by — all sent to the client. The DTO whitelist approach means you explicitly include each field. If you forget a field, it is not sent (safe). Without a DTO, if you add a sensitive column, it is automatically exposed (dangerous).
- DTO function: toUserDTO(model) — explicit whitelist of fields to expose
- Database model and API contract evolve independently — DTO is the bridge
- Whitelist (include only listed fields) not blacklist (exclude sensitive fields)
- DTO includes formatting: dates to ISO 8601, enums to strings, nested objects shaped
- One DTO per API version — v1 and v2 can shape the same model differently
Without DTOs, adding a sensitive column to the database automatically exposes it in the API. With DTOs, you explicitly whitelist every field. Forgot a field? It is not sent (safe default). The DTO is a firewall between your database schema and your API contract.
Rule 2: ISO 8601 Dates in UTC
The rule: 'All dates in API responses must be ISO 8601 format in UTC: 2026-03-27T14:30:00.000Z. The Z suffix indicates UTC — no timezone ambiguity. Never return: Unix timestamps (1711547400 — unreadable without conversion), localized strings ('March 27, 2026 2:30 PM' — locale-dependent, unparseable in many languages), or database-native formats ('2026-03-27 14:30:00+00' — varies by database driver).'
For timezone handling: 'Store dates in UTC in the database. Return dates in UTC in the API. Let the client convert to the user local timezone for display. This eliminates: timezone bugs (server in US-East, user in Tokyo, database in UTC — which timezone is the date?), DST issues (UTC has no daylight saving transitions), and ambiguity (2:30 AM could be before or after the DST switch — UTC is unambiguous).'
AI generates: dates in whatever format the database driver returns — sometimes ISO, sometimes Unix timestamp, sometimes a Date object that JSON.stringify turns into an ISO string with inconsistent precision. Explicit .toISOString() in the DTO guarantees: ISO 8601, UTC, millisecond precision, Z suffix, every time.
Server in US-East, user in Tokyo, database in UTC. The date says '2:30 PM' — whose 2:30 PM? ISO 8601 with Z suffix (UTC) eliminates all ambiguity. The client converts to local time for display. One format, zero timezone bugs.
Rule 3: Consistent Naming Conventions
The rule: 'Use camelCase for all JSON field names in API responses: firstName, createdAt, isActive. Never mix conventions — the database uses snake_case (first_name, created_at, is_active) but the API uses camelCase. The DTO mapper handles the transformation. Consistency means: clients define one naming convention in their models, code review catches deviations instantly, and no field is ambiguous (is it user_name or userName?).'
For arrays and nested objects: 'Arrays are plural nouns: users, articles, permissions. Nested objects follow the same camelCase rule: { user: { firstName, lastName, emailAddress } }. Boolean fields use is/has/can prefix: isActive, hasPermission, canEdit. IDs are: id for the primary resource, userId for foreign references — never user_id in the response even if that is the database column name.'
AI generates: { first_name, email_address, created_at } — snake_case from the database leaked directly to the API. JavaScript/TypeScript clients then access response.first_name, violating their own naming conventions. The DTO transforms once; every client benefits from consistent camelCase.
Rule 4: Pagination Envelopes
The rule: 'Never return bare arrays. Wrap paginated data in an envelope: { "data": [...], "pagination": { "page": 1, "perPage": 20, "total": 147, "totalPages": 8, "hasNext": true, "hasPrev": false } }. The envelope provides: total count (for UI pagination controls), page metadata (current position in the result set), and navigation hints (hasNext/hasPrev for infinite scroll or next/prev buttons).'
For cursor-based pagination: 'For large datasets or real-time data, use cursor-based pagination: { "data": [...], "pagination": { "cursor": "abc123", "hasNext": true, "perPage": 20 } }. The cursor is an opaque string (typically an encoded ID or timestamp). Cursor pagination is: stable (inserting new rows does not shift pages), performant (no OFFSET), and consistent (no skipped or duplicated items). Use cursors for feeds and timelines; use offset/page for admin tables.'
AI generates: res.json(users) — a bare array. How many total users? Unknown. Is there a next page? Unknown. What page am I on? Unknown. The client cannot build pagination UI, cannot show 'Page 2 of 8', and cannot know when to stop fetching. One wrapper object answers all of these questions.
- Never bare arrays — always envelope: { data, pagination }
- Pagination object: page, perPage, total, totalPages, hasNext, hasPrev
- Cursor-based for feeds/timelines — stable, performant, no OFFSET
- Offset-based for admin tables — simple, supports 'jump to page N'
- Include perPage in response — confirms the actual page size used
res.json(users) — a bare array. How many total? Is there a next page? What page is this? Unknown, unknown, unknown. One envelope — { data, pagination: { total, hasNext } } — and every pagination question is answered.
Rule 5: Field Filtering and Sparse Responses
The rule: 'Support field selection for API consumers who need only a subset of fields: GET /api/v1/users?fields=id,name,email. This reduces: payload size (mobile clients on slow connections), parsing time (less data to deserialize), and over-fetching (client gets exactly what it needs). Implementation: if fields param is present, filter the DTO to include only requested fields. Always include id — clients need it for caching and referencing.'
For expand/include patterns: 'For related resources, support an include parameter: GET /api/v1/articles?include=author,comments. Without include, return only IDs: { authorId: "123" }. With include, embed the related resource: { author: { id: "123", name: "Jane" } }. This gives clients control over response depth — list views get IDs (fast), detail views get embedded objects (complete).'
AI generates: every field, every time, for every client. A mobile app showing a user list needs id and name — it gets 20 fields including biography, preferences, and metadata. Fields filtering lets each client request exactly what it needs. Less bandwidth, faster responses, cleaner client code.
Complete Data Serialization Rules Template
Consolidated rules for data serialization.
- DTO pattern: never return database models — explicit whitelist of exposed fields
- ISO 8601 dates in UTC: 2026-03-27T14:30:00.000Z — always, everywhere
- camelCase for all JSON fields — DTO transforms snake_case from database
- Pagination envelope: { data, pagination } — never bare arrays
- Cursor-based for feeds, offset-based for admin — documented in API docs
- Field filtering: ?fields=id,name — sparse responses for bandwidth-sensitive clients
- Include/expand: ?include=author — control response depth per request
- Boolean prefix: isActive, hasPermission, canEdit — consistent, self-documenting