Skip to content

feat: add new way of defining api contracts#902

Merged
CatchMe2 merged 8 commits intomainfrom
api-contracts-route-contract
Mar 31, 2026
Merged

feat: add new way of defining api contracts#902
CatchMe2 merged 8 commits intomainfrom
api-contracts-route-contract

Conversation

@CatchMe2
Copy link
Copy Markdown
Contributor

@CatchMe2 CatchMe2 commented Mar 30, 2026

Changes

Summary

  • Introduces defineApiContract — a new way to define contracts that unifies REST and SSE use-cases
  • Ships inferTypes helpers (InferSuccessResponse, InferSuccessSchema, HasAnyNonJsonSuccessResponse) for consuming contract response types in application code; introduces ContractNoBody / ContractNonJsonResponse symbols for non-JSON and empty-body scenarios

Old vs new contract definition

The old buildContract / buildRestContract API threaded every aspect of the route through explicit generic type parameters. A typical payload route required up to 9 type parameters just to get full inference:

  // Old
  const contract = buildRestContract({                                                                                                                                                                    
    method: 'post',                                                                                                                                                                                       
    successResponseBodySchema: ResponseSchema,                                                                                                                                                            
    requestBodySchema: RequestBodySchema,                                                                                                                                                                 
    requestPathParamsSchema: PathSchema,                                                                                                                                                                  
    requestQuerySchema: QuerySchema,                                                                                                                                                                      
    isEmptyResponseExpected: false,                                                                                                                                                                       
    isNonJSONResponseExpected: false,                                                                                                                                                                     
    pathResolver: ({ projectId }) => `/projects/${projectId}/keys`,                                                                                                                                       
})                                                                                                                                                                                                      

The response type lived on a single successResponseBodySchema field with no per-status-code granularity, and flags like isEmptyResponseExpected / isNonJSONResponseExpected had to be specified as both a runtime value and a boolean generic to keep inference correct.

The new defineApiContract drops all explicit generics. Types are inferred entirely from the shape of the object you pass in — the same DX, with significantly less noise:

// New

  const contract = defineApiContract({
    method: 'post',
    requestBodySchema: RequestBodySchema,
    requestPathParamsSchema: PathSchema,
    requestQuerySchema: QuerySchema,
    pathResolver: ({ projectId }) => `/projects/${projectId}/keys`,                                                                                                                                       
    responseSchemasByStatusCode: {                                                                                                                                                                        
      200: ResponseSchema,                                                                                                                                                                                
      204: ContractNoBody,                                                                                                                                                                                
    },            
  })                                                                                                                                                                                                      

Per-status-code responses are first-class, and ContractNoBody / ContractNonJsonResponse symbols replace the boolean flags entirely — no generics, no duplication.

▎ Note: The new API achieves the same level of type safety without being heavily dependent on explicit generics. This results in cleaner contract definitions that are easier to read, write, and maintain, while TypeScript still infers everything correctly.

Checklist

  • Apply one of following labels; major, minor, patch or skip-release
  • I've updated the documentation, or no changes were necessary
  • I've updated the tests, or no changes were necessary

AI Assistance Tracking

We're running a metric to understand where AI assists our engineering work. Please select exactly one of the options below:

Mark "Yes" if AI helped in any part of this work, for example: generating code, refactoring, debugging support,
explaining something, reviewing an idea, or suggesting an approach.

  • Yes, AI assisted with this PR
  • No, AI did not assist with this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b4dc9b64-0b42-4838-9e5d-6b7572f4bd4b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces a new type-safe REST API contract system within the @lokalise/api-contracts package. It adds a factory function defineApiContract for defining strongly-typed API routes with method, path parameters, and response schemas. The system supports multiple response kinds through discriminated unions: JSON (Zod schemas), text, blob, SSE (server-sent events), and composite responses via anyOfResponses. Runtime utilities include resolveContractResponse for matching responses by content-type, path rendering via mapApiContractToPath, and schema extraction helpers. Compile-time type utilities enable inference of successful responses, SSE event types, and available response modes. Supporting infrastructure includes HTTP status code constants, type-level utility functions, and comprehensive test coverage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add new way of defining api contracts' directly describes the main change: introducing a new defineRouteContract API that provides an alternative method for defining API contracts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description is comprehensive and complete, covering all required sections with substantial detail about the changes, migration path, and checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch api-contracts-route-contract

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app/api-contracts/src/new/contractResponse.ts`:
- Around line 82-102: The current matchTypedResponse uses substring includes
which is brittle; modify matchTypedResponse to parse the incoming contentType
into a MIME-aware media type (strip parameters after ';', trim and lowercase)
and then perform exact comparisons: for text/blob/sse compare the normalized
media type to the normalized entry.contentType (for SSE check equality to
'text/event-stream'), and for JSON treat media types that are exactly
'application/json' or that endWith('+json') (case-insensitive) as JSON; update
the logic in matchTypedResponse (and any comparisons involving entry.contentType
or contentType) to use this normalized media type instead of includes so
matching is robust to case, parameters, and vendor suffixes.

In `@packages/app/api-contracts/src/new/README.md`:
- Around line 129-131: Update the README to reflect the actual required type for
requestPathParamsSchema: change the documented type from z.ZodType to
z.ZodObject and add a short note that path params must be an object schema,
matching the RequestPathParamsSchema definition in defineApiContract.ts so users
don't try to pass non-object Zod schemas for path parameters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b133c223-8efc-4b98-aa46-ea6fdd4509fe

📥 Commits

Reviewing files that changed from the base of the PR and between f800c0f and fcca08f.

📒 Files selected for processing (11)
  • packages/app/api-contracts/src/HttpStatusCodes.ts
  • packages/app/api-contracts/src/index.ts
  • packages/app/api-contracts/src/new/README.md
  • packages/app/api-contracts/src/new/constants.ts
  • packages/app/api-contracts/src/new/contractResponse.spec.ts
  • packages/app/api-contracts/src/new/contractResponse.ts
  • packages/app/api-contracts/src/new/defineApiContract.spec.ts
  • packages/app/api-contracts/src/new/defineApiContract.ts
  • packages/app/api-contracts/src/new/inferTypes.spec.ts
  • packages/app/api-contracts/src/new/inferTypes.ts
  • packages/app/api-contracts/src/typeUtils.ts

Copy link
Copy Markdown
Collaborator

@CarlosGamero CarlosGamero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@CatchMe2 CatchMe2 force-pushed the api-contracts-route-contract branch from 9c71dce to 2a0003b Compare March 31, 2026 15:01
@CatchMe2 CatchMe2 merged commit b0f9945 into main Mar 31, 2026
6 checks passed
@CatchMe2 CatchMe2 deleted the api-contracts-route-contract branch March 31, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants