Backend

Design a small API contract before generating frontend and backend code

Kamruz
Kamruz
Oct 14, 2026
5 min read
Design a small API contract before generating frontend and backend code

A task creation form sends dueDate as an empty string. The backend expects either an ISO timestamp or no field, and the generated client expects the response to contain taskId. The server returns id. Both files compile independently because their types were written separately. The first real request fails, and each side's author believes the other side violated an obvious convention.

The smallest repair starts with a shared observable contract for one operation. This example uses a Node-based JSON API and MongoDB storage, but the contract work applies to other stacks. Library-specific validation syntax is intentionally omitted so the discussion does not imply that TypeScript types validate untrusted network input at runtime.

Describe one operation precisely

Choose POST /tasks and state what success means: an authenticated actor creates a task in an organization they may access. Define the required title, optional due date, and response identifier. Specify whether an omitted date and a null date mean the same thing. Avoid using an empty string as an undocumented third state.

Write one valid request and response with synthetic values. Then write malformed JSON, missing title, unknown field, unauthorized organization, and dependency failure examples. A small set of examples exposes ambiguity earlier than generating a large schema with many unused properties.

Define title length and whitespace policy. Does trimming happen before the length check? Are line breaks allowed? Is an all-space title invalid? These are product rules that a model may guess differently on each side. Decide them explicitly and give the user a stable error that the form can display.

Keep transport and domain rules distinct

JSON parsing determines whether the body can be read. Schema validation determines whether its shape is acceptable. Domain checks determine whether the task can be created under current rules. Authorization determines whether this actor may create it in the requested organization. Storage persistence is another boundary with its own failure modes.

Do not represent all four failures as a generic validation exception. A malformed body is different from an organization the actor cannot access. Likewise, a duplicate business identifier may need a conflict response rather than an internal failure. Choose status codes according to the endpoint contract and document the stable error shape.

A useful error object contains a machine-readable code, a safe message, and optional field errors. It should not include a stack trace, database query, or private ownership information. The client can translate the code into UI while preserving a request identifier for support.

Define runtime validation and public mapping

Treat the decoded request as unknown until validation succeeds. Do not spread it directly into a database insert. Build the allowed persistence fields after checking shape and access. This prevents a caller from setting internal properties such as completion state, owner, or administrative flags.

{
"title": "Review the release checklist",
"dueAt": null,
"organizationId": "org_test_24"
}

The example uses an application identifier representation; a real MongoDB ObjectId boundary needs its own validated conversion. On success, return an explicit public view rather than the driver's whole result or the ORM document. Decide whether dates are strings and how identifiers are represented.

{
"id": "task_test_91",
"title": "Review the release checklist",
"dueAt": null
}

A schema or OpenAPI document can capture this shape, but it still needs examples and meaningful implementation checks. Generated types help prevent drift after the contract is established. They do not prove authorization, domain rules, or the runtime behavior of a deployed server.

Generate each side from the same evidence

Give an AI assistant the operation contract, existing repository conventions, and the narrow files it may change. Ask it to implement the server boundary and client adapter separately while preserving the shared names. Require it to identify any unresolved rule instead of inventing a date policy or organization access rule.

The server adapter should parse, validate, authorize, call the domain operation, and map the result. The client adapter should send the specified body and decode success and failure shapes. Keep form state conversion in one place so every empty date control produces the same wire value.

Review generated code for duplicated schemas that diverge. If the application intentionally shares a schema package, keep browser-safe dependencies separate from server-only storage code. A shared type file should not accidentally import a database client into the frontend bundle.

Verify disagreements at the boundary

Run a contract test using the actual HTTP handler with a fake or isolated storage layer, depending on the claim being tested. Send the valid example and assert public fields and types. Then send missing title, whitespace title, invalid date, unknown field, and an unauthorized organization.

Decide whether unknown fields are rejected or ignored, and test that decision. Either policy can be valid, but silently accepting privileged fields is not. Test explicit null and omitted optional values separately if they have different semantics. A test using only a perfectly formed object cannot expose the original empty-string bug.

Add a client adapter test with representative responses. It should fail when taskId is read from a response that only provides id. Do not mock the entire adapter with the expected final object; that bypasses the contract conversion being tested. State which checks exercise HTTP parsing and which exercise only local mapping.

Handle evolution deliberately

An additive response field is often easier to introduce than renaming a required field, but compatibility depends on clients' decoding rules. A strict client schema may reject unknown fields. Review actual clients before declaring a change backward compatible.

If a field is renamed, use a bounded transition or a versioned contract rather than changing both sides under an assumption that all deployed clients update together. Browser tabs, mobile applications, and third-party integrations can remain on earlier versions. Document the deprecation and the point when the old name can be removed.

For create operations, consider duplicate submission and network ambiguity as a separate contract concern. Idempotency keys cover that behavior. Do not smuggle retry semantics into a generic validation field without defining their lifetime and scope.

Keep the contract small enough to review

A one-operation contract should make a real request reviewable in minutes. Include only fields and outcomes the feature needs. Avoid generating a universal response envelope, elaborate inheritance tree, or speculative API version scheme before there is a concrete compatibility problem.

Invalid input classification explains the next diagnostic step when a boundary is already failing. Authorization tracing follows the actor rule from that contract to storage. The contract's value is that both implementation and verification now refer to the same observable behavior.

Sources

About the Author

Kamruz

Kamruz

Full Stack Developer

A passionate developer who loves creating modern web applications and sharing knowledge with the community.

More About Me

Related Articles