Backend & Data
Backend & Data
Implement backend functionality that is correct beyond the happy path: data integrity, authorization, concurrency, and failure handling are first-class requirements, not afterthoughts.
Before writing code, answer these four questions from actual project evidence (not assumption):
- What already exists? Read the relevant route/controller, model/schema, migration history, and existing tests.
- What are the real contracts? Existing request/response shapes, status codes, error formats, auth mechanism — don't invent new conventions.
- What must stay consistent? Which invariants, constraints, or multi-step operations need transactional or concurrency protection.
- What is the smallest correct change? Don't add versioning, pagination, or zero-downtime migration machinery the project doesn't need or use elsewhere.
Then implement, applying the checklists below, and verify with the project's actual test/lint/build tools.
Progress:
- [ ] Inspect existing code, schema, migrations, tests, dependency versions
- [ ] Identify invariants, ownership rules, and consumers affected
- [ ] Implement validation, business logic, and persistence
- [ ] Apply transactions/concurrency control where multiple writes must be atomic
- [ ] Apply authorization checks at the resource/field level
- [ ] Handle error paths with safe, consistent responses
- [ ] Write/update migration with compatibility and rollback considered
- [ ] Run tests, type checks, lint, and manual/integration verification
- [ ] Report what was verified vs. assumed vs. unknown
1. Inspect before implementing
- Scale inspection to task size: a single-field validation fix needs only the touched file and its tests; a cross-service contract change or migration needs the schema, migration history, consumers, and related services.
- Never invent endpoints, fields, model names, library APIs, or versions. If uncertain, check the dependency manifest/lockfile, official docs, or state explicitly what is unverified.
2. Implement the logic
- Match existing architecture layering (controller/service/repository, or whatever the project uses) — don't introduce a new pattern unilaterally.
- Validate all untrusted input server-side: type, required fields, length/range, allowed field set. Reject unknown/protected fields explicitly (don't mass-assign request bodies to models).
- Map failure conditions to correct status codes/error shapes consistent with existing error handling in the codebase.
- Don't leak stack traces, internal identifiers, or secrets in error responses or logs.
3. Protect data integrity
- Use DB-level constraints (PK/FK/unique/not-null/check) for invariants that matter, not just app-level checks.
- Wrap multi-step writes in a transaction; decide explicitly what happens on partial failure (rollback, compensating action, or idempotent retry).
- Consider concurrent access: use row locks, optimistic concurrency (version/updated_at columns), or unique constraints where two requests could race.
- Always use parameterized queries / the ORM's safe query builder — never string-concatenate untrusted input into SQL.
4. Enforce access control
- Every operation touching a resource must verify the caller is authorized for that specific resource, not just authenticated. Never trust a client-supplied ID as proof of ownership.
- Reuse the existing auth/session mechanism; don't build a parallel one.
- Check HTTP method/operation semantics: GET/HEAD must stay safe (no side effects); only retry idempotent operations automatically. For non-idempotent operations exposed to retries (payments, order creation), use an idempotency key or dedup check.
5. Handle migrations safely
- Read existing migration history and current production-like data assumptions first.
- For additive, low-risk changes (new nullable column, new table): a single straightforward migration is fine.
- For risky changes (new required column, column rename/drop, type change on populated table): plan backfill + compatibility window (e.g., add nullable → backfill → add NOT NULL constraint in a later step) and verify old application code won't break mid-rollout if deploy order isn't atomic.
- Never silently drop data or columns without explicit authorization. Have a rollback/recovery note for destructive changes.
- Verify engine-specific behavior (e.g., does
ALTER TABLE ADD COLUMN NOT NULLlock the table on this DB version?) rather than assuming.
6. Verify
- Run the project's real test suite, type checker, and linter — don't claim success without executing them.
- For data/migration changes, verify against actual (or realistic sample) data, not just an empty test DB, when feasible.
- State clearly what was verified, what was assumed, and what remains unchecked (e.g., "not tested against production data volume").
7. Coordinate, don't over-escalate
Hand off only when genuinely warranted:
- architecture — new service boundaries, cross-component data ownership shifts.
- frontend — contract changes affecting UI consumers.
- debugging — root cause of a failure is not yet known.
- security — deep auth/injection/sensitive-data review beyond standard checks.
- performance — measurable bottleneck investigation.
- testing-quality — broad regression/integration coverage.
Routine CRUD, validation, and migration work stays within this skill.
A. Add a CRUD endpoint to an existing app
- Inspect: existing route structure, naming conventions, response envelope, auth middleware, equivalent existing endpoints.
- Decisions: reuse existing validation/middleware patterns; define DTO/response shape distinct from raw DB model if the project already does this; correct status codes (201 on create, 404 on missing, 409 on conflict).
- Avoid: inventing a new response envelope; adding a dependency (e.g., a new validation library) when one is already in use; exposing internal-only fields.
- Verify: endpoint tested for create/read/update/delete, invalid input rejected, run existing test suite.
B. Reject malformed/disallowed fields
- Inspect: the model's writable fields vs. server-controlled fields (id, timestamps, role, ownerId).
- Decisions: explicit allow-list for mass assignment; validate types/formats/lengths before hitting the DB; use parameterized queries regardless of validation.
- Avoid: passing raw
req.bodyto an ORMupdate/createcall; relying on client-side validation; building SQL via string concatenation. - Verify: test with extra/forbidden fields (e.g.,
role: "admin"), malformed types, and oversized payloads — confirm rejection with a clear 400 and no DB write occurred.
C. Prevent cross-user resource access via ID substitution
- Inspect: how the resource's ownership is modeled (owner_id/user_id field, join table, etc.) and current authorization middleware.
- Decisions: every read/update/delete must filter or check against
current_userownership/role, not just existence of the ID; return 404 (not 403) if leaking existence is undesirable, per existing convention. - Avoid: trusting a path/body ID alone; checking ownership only on some HTTP verbs but not others (e.g., GET unprotected but DELETE protected).
- Verify: write a test where user A requests/modifies user B's resource ID and confirm denial; confirm legitimate owner access still succeeds.
D. Multi-step business operation requiring consistency
- Inspect: all tables/entities touched, whether DB supports transactions, existing transaction usage patterns in the codebase.
- Decisions: wrap all writes in a single transaction; decide rollback vs. compensating action if an external (non-transactional) call is involved (e.g., payment API + DB write — use an outbox/idempotent reconciliation pattern rather than a bare transaction).
- Avoid: partial writes left uncommitted on error; assuming an external API call can be rolled back by a DB transaction.
- Verify: simulate failure at each step (e.g., force an exception after step 1 of 2) and confirm no partial state persists; check DB directly, not just the HTTP response.
E. Add a required field to an existing table
- Inspect: current row count/nature of existing data, deployment process (single-step vs. rolling deploy), consumers reading/writing this table.
- Decisions: for populated tables, migrate in stages — add nullable column → backfill existing rows → add NOT NULL/default constraint in a follow-up migration; ensure old app code (if still running during rollout) won't break on the interim schema.
- Avoid: adding a NOT NULL column with no default directly to a populated table in one step; assuming zero rows exist without checking.
- Verify: run migration against a copy of real/realistic data; confirm existing records end up valid; confirm old and new app versions both function during the compatibility window if rolling deploys are used.
F. Repeated request after uncertain network outcome
- Inspect: whether the operation is naturally idempotent (e.g.,
PUTwith full resource state) or not (e.g.,POST /charge,POST /orders). - Decisions: for non-idempotent operations exposed to client retry, require/generate an idempotency key (stored with a unique constraint) and return the original result on duplicate submission instead of re-executing.
- Avoid: blind automatic retries on POST/non-idempotent endpoints; relying on "it probably won't happen twice."
- Verify: submit the same request twice with the same idempotency key and confirm only one side effect (e.g., one charge, one row) occurs.
G. List endpoint with pagination/limits
- Inspect: expected data volume, existing pagination conventions in the codebase (cursor vs. offset, query param names).
- Decisions: apply a sane default and maximum page size; use cursor-based pagination for large/frequently-changing datasets, offset pagination otherwise; add filtering only as justified by actual use case.
- Avoid: unbounded
SELECT *on a table that can grow large; reinventing pagination style inconsistent with the rest of the API. - Verify: test with page-size boundary values (0, max+1, negative), confirm response includes pagination metadata consistent with project convention.
- Match the project's existing style/framework idioms rather than introducing new patterns or libraries.
- Prefer DB constraints over "we'll validate it in the app" for anything that must never be violated.
- Make illegal states unrepresentable in the data model where feasible, rather than patching with extra validation.
- Keep error responses consistent and documented; distinguish client errors (4xx) from server errors (5xx) precisely.
- Treat every externally-reachable write path as a target for malformed/malicious input, regardless of whether a frontend "already validates."
- Trusting a resource ID in the request as proof of ownership.
- Validating only on the frontend and skipping backend validation "because the UI already checks."
- Using application-level checks alone for uniqueness/foreign-key integrity instead of DB constraints.
- Running multi-step writes outside a transaction and assuming failures "won't happen."
- Dropping columns/tables or adding NOT NULL constraints on populated tables without a staged plan.
- Retrying non-idempotent operations (payments, order creation) automatically after timeouts.
- Returning raw stack traces, SQL errors, or internal object structures to clients.
- Adding pagination/versioning/zero-downtime migration ceremony to a small project where it adds no value — match effort to actual risk and scale.