feat(endpoint): add ctx.json helper for JSON responses#42
feat(endpoint): add ctx.json helper for JSON responses#42halvaradop wants to merge 1 commit intomasterfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe changes introduce type-safe JSON response handling for route handlers by adding new utility types and helper functions. A Changes
Sequence DiagramsequenceDiagram
participant Client
participant Router as Router<br/>(handleRequest)
participant Context as RequestContext
participant Handler as Route Handler
participant Inference as inferHandlerResponse
Client->>Router: HTTP Request
activate Router
Router->>Context: Create GlobalRequestContext<br/>(with json helper)
activate Context
Router->>Handler: Execute handler<br/>(ctx)
deactivate Context
activate Handler
Handler->>Context: Call ctx.json(data)
activate Context
Context-->>Handler: JsonResponse<T>
deactivate Context
Handler-->>Inference: Return JsonResponse | Response |<br/>string | undefined | etc.
deactivate Handler
activate Inference
Inference->>Inference: Normalize result:<br/>- Response → keep<br/>- undefined → 204<br/>- string → text/plain<br/>- object/array → JSON
Inference-->>Router: HTTP Response
deactivate Inference
Router-->>Client: Response
deactivate Router
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip CodeRabbit can generate a title for your PR based on the changes.Add |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
test/type.test-d.ts (1)
465-471: AssertRouteEndpointResponse<typeof endpoint>in this case.This block only proves the handler keeps its
JsonResponsereturn. It doesn't exercise the new exported helper type, soRouteEndpointResponsecan regress without failing this test.🧪 Suggested addition
expectTypeOf<ReturnType<typeof endpoint.handler>>().toEqualTypeOf<JsonResponse<{ id: number }>>() + expectTypeOf<RouteEndpointResponse<typeof endpoint>>().toEqualTypeOf<{ id: number }>() const router = createRouter([endpoint])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/type.test-d.ts` around lines 465 - 471, Add an assertion that verifies the exported helper type RouteEndpointResponse uses the same inferred response as the endpoint: after creating endpoint with createEndpoint("GET", "/test", (ctx) => ctx.json({ id: 1 })), add an expectTypeOf<RouteEndpointResponse<typeof endpoint>>().toEqualTypeOf<JsonResponse<{ id: number }>>() (or add it alongside the existing handler return assertion) so the test exercises RouteEndpointResponse and will break if that helper regresses; reference the symbols endpoint, RouteEndpointResponse, JsonResponse, and createEndpoint when adding the new expectTypeOf assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/types.ts`:
- Around line 112-145: The JsonResponse<T> type is structurally ambiguous with
the DOM Response because Response.json(): Promise<any> matches the shape; add a
unique nominal brand (e.g., a readonly unique symbol property) to
JsonResponse<T> so it no longer matches plain Response, and update the json
helper in src/context.ts to return an object that includes that same brand so
InferRouteHandlerJsonResponse correctly extracts T; ensure the brand symbol name
is exported/used consistently between the JsonResponse<T> definition and the
json helper (refer to JsonResponse, InferRouteHandlerJsonResponse, and the json
helper).
---
Nitpick comments:
In `@test/type.test-d.ts`:
- Around line 465-471: Add an assertion that verifies the exported helper type
RouteEndpointResponse uses the same inferred response as the endpoint: after
creating endpoint with createEndpoint("GET", "/test", (ctx) => ctx.json({ id: 1
})), add an expectTypeOf<RouteEndpointResponse<typeof
endpoint>>().toEqualTypeOf<JsonResponse<{ id: number }>>() (or add it alongside
the existing handler return assertion) so the test exercises
RouteEndpointResponse and will break if that helper regresses; reference the
symbols endpoint, RouteEndpointResponse, JsonResponse, and createEndpoint when
adding the new expectTypeOf assertion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 95a0e769-a3d1-475a-8d6e-f477a8de1126
📒 Files selected for processing (6)
src/context.tssrc/endpoint.tssrc/router.tssrc/types.tstest/client.test.tstest/type.test-d.ts
| export type JsonResponse<T> = Response & { | ||
| json(): Promise<T> | ||
| } | ||
|
|
||
| export type RouteHandlerReturn = | ||
| | Response | ||
| | JsonResponse<unknown> | ||
| | string | ||
| | number | ||
| | boolean | ||
| | null | ||
| | undefined | ||
| | Record<string, unknown> | ||
| | unknown[] | ||
| | BodyInit | ||
|
|
||
| export type InferRouteHandlerReturn<Handler> = Handler extends (...args: unknown[]) => infer R | ||
| ? R extends Promise<infer U> | ||
| ? U | ||
| : R | ||
| : never | ||
|
|
||
| export type InferRouteHandlerJsonResponse<Handler> = Handler extends (...args: unknown[]) => infer R | ||
| ? R extends Promise<infer U> | ||
| ? U extends JsonResponse<infer Data> | ||
| ? Data | ||
| : never | ||
| : R extends JsonResponse<infer Data> | ||
| ? Data | ||
| : never | ||
| : never | ||
|
|
||
| export type RouteEndpointResponse<Endpoint extends { handler: (...args: unknown[]) => any }> = InferRouteHandlerJsonResponse<Endpoint["handler"]> | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In TypeScript, is a plain Responseassignable toResponse & { json(): Promise }becauseResponse.json()returnsPromise? What unique symbolbranding pattern is recommended to distinguish a specializedResponse subtype in conditional types?
💡 Result:
No, a plain Response is assignable to Response & { json: Promise } for any T. TypeScript's standard DOM library defines Response.json: Promise, so the plain Response already satisfies having a json method returning Promise. Since Promise is assignable to Promise for any T (due to bivariant Promise generics in TypeScript), the intersection type is structurally compatible with plain Response. To distinguish a specialized Response subtype (e.g., one with json: Promise for specific T) in conditional types, use the unique symbol branding pattern: declare const brand: unique symbol; type BrandedResponse = Response & { json: Promise } & { readonly [brand]: T }; type IsTypedResponse = R extends BrandedResponse ? T : never; This creates nominal-like distinction: only Response values explicitly branded with the symbol (via type assertion after validation) will extract the T in conditional types. Plain Response lacks the brand property, so IsTypedResponse = never. Example usage: function typedJson(res: Response): BrandedResponse { // Validate res.ok, content-type, etc. return res as BrandedResponse; } type ExtractT = R extends BrandedResponse ? T : never; const typed = typedJson<{ foo: string }> fetchResult); type FooType = ExtractT; // { foo: string } type PlainType = ExtractT; // never This pattern uses unique symbols to avoid structural typing pitfalls, as recommended across TypeScript community resources on branded types.
Citations:
- 1: https://typhonjs-typedoc.github.io/ts-lib-docs/2023/dom/interfaces/Response.html
- 2: https://developer.mozilla.org/en-US/docs/Web/API/Response/json
- 3: v3.0.0
await res.json()isunknownnode-fetch/node-fetch#1262 - 4: Update Body json() method to allow generic types. microsoft/TypeScript-DOM-lib-generator#1711
- 5: https://oneuptime.com/blog/post/2026-01-30-how-to-implement-branded-types-in-typescript/view
- 6: https://blog.logrocket.com/leveraging-typescript-branded-types-stronger-type-checks/
- 7: https://mikepalmer.dev/blog/stricter-types-in-typescript-with-brands
- 8: https://dev.to/themuneebh/typescript-branded-types-in-depth-overview-and-use-cases-60e
- 9: https://www.glassthought.com/notes/sbhkldvzby5juczm3vmsupt
- 10: https://prosopo.io/blog/typescript-branding/
- 11: https://shramko.dev/snippets/branded-types
🏁 Script executed:
cat -n src/types.ts | sed -n '112,145p'Repository: aura-stack-ts/router
Length of output: 1200
🏁 Script executed:
rg -n "InferRouteHandlerJsonResponse|RouteEndpointResponse|JsonResponse" src/Repository: aura-stack-ts/router
Length of output: 989
🏁 Script executed:
rg -A 5 -B 5 "InferRouteHandlerJsonResponse" src/endpoint.tsRepository: aura-stack-ts/router
Length of output: 346
🏁 Script executed:
cat -n src/endpoint.ts | head -100Repository: aura-stack-ts/router
Length of output: 4417
🏁 Script executed:
rg -n "RouteEndpointResponse" src/Repository: aura-stack-ts/router
Length of output: 226
🏁 Script executed:
rg -n "export.*RouteEndpointResponse|export.*InferRouteHandlerJsonResponse" src/Repository: aura-stack-ts/router
Length of output: 344
🏁 Script executed:
rg -n "createEndpoint" src/ | head -20Repository: aura-stack-ts/router
Length of output: 1259
🏁 Script executed:
cat -n src/index.tsRepository: aura-stack-ts/router
Length of output: 479
🏁 Script executed:
git ls-files --others --exclude-standard | head -20Repository: aura-stack-ts/router
Length of output: 74
🏁 Script executed:
fd -e "test\|spec" src/Repository: aura-stack-ts/router
Length of output: 385
🏁 Script executed:
fd "test|spec" src/Repository: aura-stack-ts/router
Length of output: 46
🏁 Script executed:
cat -n src/types.ts | sed -n '145,200p'Repository: aura-stack-ts/router
Length of output: 3048
🏁 Script executed:
rg -n "typeof.*Handler|type.*Handler" src/types.ts | head -30Repository: aura-stack-ts/router
Length of output: 764
🏁 Script executed:
cat -n src/context.ts | sed -n '150,160p'Repository: aura-stack-ts/router
Length of output: 340
🏁 Script executed:
cat -n src/types.ts | sed -n '220,240p'Repository: aura-stack-ts/router
Length of output: 1025
🏁 Script executed:
rg -n "InferRouteHandlerJsonResponse|RouteEndpointResponse" src/router.tsRepository: aura-stack-ts/router
Length of output: 46
Add branding to JsonResponse<T> to prevent structural type ambiguity.
Response.json() already returns Promise<any> in the DOM types, so plain Response structurally satisfies JsonResponse<T>. This causes InferRouteHandlerJsonResponse to extract any for handlers returning a normal Response, undermining the inference API. A unique symbol brand fixes this false positive:
Recommended fix
+declare const jsonResponseBrand: unique symbol
-export type JsonResponse<T> = Response & {
- json(): Promise<T>
-}
+export interface JsonResponse<T> extends Response {
+ readonly [jsonResponseBrand]: T
+ json(): Promise<T>
+}Then update the json helper in src/context.ts to apply the brand:
-export const json = <T>(data: T, init?: ResponseInit): JsonResponse<T> => {
- return Response.json(data, init) as JsonResponse<T>;
+const brand = Symbol('JsonResponse');
+export const json = <T>(data: T, init?: ResponseInit): JsonResponse<T> => {
+ return Object.assign(Response.json(data, init), { [brand]: undefined }) as JsonResponse<T>;
+}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/types.ts` around lines 112 - 145, The JsonResponse<T> type is
structurally ambiguous with the DOM Response because Response.json():
Promise<any> matches the shape; add a unique nominal brand (e.g., a readonly
unique symbol property) to JsonResponse<T> so it no longer matches plain
Response, and update the json helper in src/context.ts to return an object that
includes that same brand so InferRouteHandlerJsonResponse correctly extracts T;
ensure the brand symbol name is exported/used consistently between the
JsonResponse<T> definition and the json helper (refer to JsonResponse,
InferRouteHandlerJsonResponse, and the json helper).
Description
ctx.json
Summary by CodeRabbit
New Features
jsonmethod to request context for type-safe JSON responses.Tests