---
url: /contracts/API_AND_SCHEMA_DISCIPLINE.md
---
# API and Schema Design Discipline

Status: **mandatory design rules for `ninaku-app-api`**.

This document complements [`ARCHITECTURE.md`](../reference/ARCHITECTURE.md). It exists to prevent two common failure modes as Ninaku grows: creating APIs that mirror database tables without expressing business intent, and expanding the database with tables that have no clear owner or real operational purpose.

## 1. APIs must have business meaning

Ninaku does **not** expose an API simply because a table exists.

An endpoint must represent at least one of these things:

* a real business action;
* a real query needed by an operator, screen or integration;
* an explicit workflow transition;
* a stable public contract between Ninaku and an external consumer.

The default question is not **"what CRUD endpoints does this table need?"**. The default questions are:

1. What is the user or system trying to accomplish?
2. Which module owns that operation?
3. Which invariants must be protected?
4. What is the smallest stable contract that expresses that use case?

Bad API design:

```text
POST /sale-lines
PATCH /sale-lines/{id}
POST /payment-attempts
POST /inventory-movements
POST /journal-entries
```

when those endpoints merely expose persistence structures and allow callers to assemble domain state themselves.

Preferred API design:

```text
POST /api/v1/sales/orders
POST /api/v1/sales/orders/{id}/confirm
POST /api/v1/sales/orders/{id}/cancel

POST /api/v1/inventory/transfers
POST /api/v1/inventory/transfers/{id}/dispatch
POST /api/v1/inventory/transfers/{id}/receive

POST /api/v1/payments/intents
POST /api/v1/payments/{id}/capture
POST /api/v1/payments/{id}/refund
```

The backend coordinates the necessary tables and modules internally. The client should not need to understand the physical database design in order to perform a business operation.

## 2. Do not create one endpoint per table

Database tables and API resources are different abstractions.

A module may use several tables to implement one business capability, and one screen query may read several module-owned projections through explicit contracts.

Therefore:

* there is no requirement for every table to have a controller;
* there is no requirement for every table to have a repository exposed to other modules;
* there is no requirement for every table to have create/read/update/delete endpoints;
* internal persistence tables may never appear in the public API;
* lifecycle transitions should be modeled explicitly instead of arbitrary record mutation.

For example, confirming a sale may atomically update sales state, inventory reservations, tax results and outbox records. The public operation is still **confirm sale**, not four unrelated CRUD calls.

## 3. APIs should be designed around workflows and screens

Operational APIs should make normal product flows efficient without turning endpoints into uncontrolled "mega APIs".

For each screen or workflow, define:

* the business purpose;
* required data for initial operation;
* optional/heavy data loaded on demand;
* commands available from that state;
* authorization scope;
* pagination/cursor behavior;
* consistency requirements;
* latency/query-count/response-size expectations.

Prefer a small number of meaningful calls over dozens of table-shaped calls. A query composer may combine read contracts from multiple modules when a screen genuinely requires it, while write operations remain owned by explicit use cases.

Do not optimize only for fewer HTTP requests. Also optimize:

* SQL query count;
* N+1 avoidance;
* payload size;
* stable pagination;
* indexes used by real access patterns;
* caching only where correctness permits it.

Section 12 covers how the resulting response is declared in OpenAPI.

## 4. Every table must justify its existence

Every canonical table must have a clear reason to exist.

For each table, we should be able to answer:

1. **Owner:** Which module owns this table?
2. **Business fact:** What fact or lifecycle does it represent?
3. **Writer:** Which use case/function is allowed to create or mutate it?
4. **Reader:** Which use case, report, projection or integration consumes it?
5. **Invariant:** What rule, uniqueness requirement, history or audit need requires persistence here?
6. **Lifecycle:** How is the row created, changed, closed/expired or retained?
7. **Why separate:** Why can this not live safely in an existing table/aggregate/projection?

If these questions cannot be answered, the table is a candidate for redesign, consolidation or removal.

A table must not exist only because "we may need it someday".

## 5. Existing canonical tables must be traced to real use

The current PostgreSQL baseline is mature and contains many domain tables. As application modules are implemented, each relevant table must be connected to a real application path instead of being ignored indefinitely.

That does **not** mean every table needs a public API. It means every table kept in the canonical schema should eventually have an understood role such as:

* command/write model;
* domain history/ledger;
* read projection;
* integration inbox/outbox state;
* audit/security evidence;
* configuration/reference data;
* operational state required by another explicit mechanism.

During module implementation, unused or redundant structures should be identified and reviewed. Do not preserve dead complexity merely because it already exists.

Removal or consolidation of an existing table must still respect migrations, historical data, constraints, consumers and compatibility.

## 6. Creating a new table requires a design reason

A new table should be introduced only when the domain or operational requirement genuinely needs new persistent state.

Before adding one, evaluate whether the requirement can be satisfied by:

* an existing aggregate/table;
* an existing ledger/history table;
* an additional constrained column;
* an existing relation;
* a derived read model/view;
* an existing module contract;
* a typed integration bridge;
* a normalized reference structure already present.

Do not force everything into one table merely to reduce table count, but also do not normalize mechanically until every concept becomes its own table.

The decision must optimize for **correctness, ownership, clarity, real query patterns and maintainability**.

## 7. Schema optimization is encouraged when evidence supports it

The canonical SQL is a contract, not a museum.

If implementation work reveals that the schema can be made simpler, safer or faster, it may be refined through an explicit migration and documented reasoning.

Examples of valid optimization work:

* removing duplicated state whose owner is already clear elsewhere;
* consolidating structures that model the same lifecycle unnecessarily;
* splitting a mixed table when two independent ownership/lifecycle rules are being forced together;
* replacing repeated expensive queries with a justified projection;
* adding/removing/reordering indexes based on actual access patterns;
* tightening constraints so invalid states are impossible;
* reducing optional cross-domain coupling;
* moving a table to the module/schema that actually owns it;
* eliminating unused columns or tables after compatibility analysis.

Optimization must never mean silently weakening domain guarantees or tenant isolation for convenience.

## 8. No speculative database design

Do not create tables, columns, endpoints or modules just to make the architecture "look complete".

A proposed structure should have at least one concrete use case and a clear owner before implementation.

This applies especially to future verticals. Hospitality, Retail or Restaurant extensions should be introduced from real workflows, not by generating every possible entity in advance.

Prefer:

> requirement -> domain owner -> use case -> contract -> persistence decision

instead of:

> imagined table -> CRUD -> service -> endpoint -> search for a use case later

## 9. Schema changes must include impact review

Any new table or meaningful schema change should review:

* module ownership;
* tenant/RLS behavior;
* Business Unit / Legal Entity scope where relevant;
* foreign-key direction and cross-module dependency;
* transaction boundaries;
* idempotency/concurrency implications;
* offline/sync implications;
* audit/history requirements;
* migration and backward compatibility;
* expected query patterns and indexes;
* retention/privacy requirements where applicable;
* effect on canonical database tests.

A schema PR should explain the business use case, not only the DDL.

## 10. Definition of done for a new business capability

A capability is not finished merely because its tables and endpoints exist.

Before considering it complete, verify that:

* API operations express business intent;
* authorization and tenant isolation are enforced;
* relevant existing schema structures are actually used or explicitly declared out of scope;
* writes preserve module ownership;
* idempotency/concurrency rules are covered where required;
* database constraints support the invariant rather than relying only on controller validation;
* queries are bounded and indexed for the expected access pattern;
* unnecessary tables/endpoints have not been introduced;
* tests cover the business path, not only repository CRUD.

## 11. Practical rule

For every proposed endpoint, ask:

> **Would this endpoint still make sense to a product/domain person who does not know our table names?**

If the answer is no, reconsider the API.

For every proposed table, ask:

> **What real business fact requires this table, who owns it, who writes it, who reads it, and why cannot the existing model represent it correctly?**

If there is no strong answer, do not create the table yet.

## 12. Declaring the response contract in OpenAPI

Every non-exempt JSON response leaves the application wrapped by `src/foundation/http/envelope/http-envelope.interceptor.ts`: whatever the handler returns becomes `data`, and a handler returning `HttpResponseWithMeta` also gets `meta`. A controller that documents the bare payload therefore publishes a contract the runtime does not honour.

Two decorators in `src/foundation/openapi/api-envelope-response.decorator.ts` declare the wrapped shape. Both take the Zod schema the route really returns:

| Decorator | Use it for | Documented body |
|---|---|---|
| `ApiDataResponse` | A single resource or a command result | `Envelope` with `data` resolved to that schema |
| `ApiCollectionResponse` | One page of a collection | `Envelope` with `data` as an array of that schema and `meta.pageInfo` required |

```ts
@Get(':organizationId')
@ApiDataResponse({ name: 'Organization', schema: OrganizationSchema })
findOne(): Organization;

@Get()
@ApiCollectionResponse({ name: 'Product', schema: ProductSchema })
list(): HttpResponseWithMeta<Product[], { pageInfo: PageInfo }>;

@Post()
@ApiDataResponse({
  name: 'SalesOrder',
  schema: SalesOrderSchema,
  status: HttpStatus.CREATED,
})
create(): SalesOrder;
```

`status` documents a route that answers with something other than 200; `description` documents what the response means when the status alone does not say it.

Both decorators compose onto the shared `Envelope` component with `allOf` instead of repeating the envelope at every endpoint, so the envelope keeps a single definition in `tooling/openapi/shared-components.ts` and a client can see the lineage in the generated document. The `data` schema is derived from Zod through `convertZodSchemaToOpenApiSchema`, never hand-written beside it, so a schema with no OpenAPI 3.0 equivalent fails loudly instead of publishing an empty `{}`.

A route that deliberately opts out of the wrapper with `SkipHttpEnvelope` must not use these decorators. It returns an unwrapped body, so it documents that body directly with the plain `@nestjs/swagger` decorators, exactly as the health probes do.

`test/fixtures/openapi-envelope/openapi-envelope-document.spec.ts` asserts the emitted document for each of these cases, including the opt-out.

***

Ninaku should prefer a smaller number of coherent APIs and justified persistence structures over a large number of CRUD endpoints and speculative tables. The objective is not minimum table count or minimum endpoint count by itself; the objective is a model where every piece has a clear reason to exist and the system remains understandable as more verticals are added.
