Skip to content

feat: add getPayload and getValidatedPayload utilities#1339

Draft
productdevbook wants to merge 2 commits intomainfrom
feat/get-payload
Draft

feat: add getPayload and getValidatedPayload utilities#1339
productdevbook wants to merge 2 commits intomainfrom
feat/get-payload

Conversation

@productdevbook
Copy link
Copy Markdown
Member

@productdevbook productdevbook commented Mar 15, 2026

Summary

Closes #785

Adds getPayload and getValidatedPayload utilities that merge route params, query params, and body into a single object — depending on the request method.

As pi0 noted: "Idea is that a utility to work with both query/body."

getPayload(event)

// GET /search/books?q=h3 → merges route params + query
app.get("/search/:category", async (event) => {
  const payload = await getPayload(event);
  // { category: "books", q: "h3" }
});

// POST /users/123 with body { name: "Alice" } → merges route params + body
app.post("/users/:id", async (event) => {
  const payload = await getPayload(event);
  // { id: "123", name: "Alice" }
});

getValidatedPayload(event, schema)

app.post("/users/:id", async (event) => {
  const payload = await getValidatedPayload(event, z.object({
    id: z.string(),
    name: z.string(),
  }));
  // Validated and typed
});

Behavior

Method Sources merged
GET, HEAD route params + query params
POST, PUT, PATCH, DELETE route params + parsed body

Route params have lowest priority — body/query values override them on conflict.

Test plan

  • GET returns query merged with route params
  • POST returns body merged with route params
  • Body overrides route params on conflict
  • getValidatedPayload validates with zod schema
  • getValidatedPayload throws 400 on invalid data
  • All tests pass in web + node (14/14)
  • Typecheck clean, formatting clean

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced payload utilities for extracting and validating request data. Query parameters, request bodies, and route parameters are automatically merged with configurable priority. Built-in support for validating against standard schemas or custom validators with type-safe results.
  • Tests

    • Added comprehensive test coverage for new payload utilities.

productdevbook and others added 2 commits March 15, 2026 10:39
Merges route params, query params, and body into a single object:

- GET/HEAD: route params + query params
- POST/PUT/PATCH/DELETE: route params + parsed body

Route params have lowest priority — body/query values override them.

Usage:
  app.post("/users/:id", async (event) => {
    const payload = await getPayload(event);
    // { id: "123", name: "Alice" }
  });

Closes #785

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add getValidatedPayload(event, validate, options?) following the same
pattern as getValidatedQuery and readValidatedBody. Validates the
merged payload (route params + query/body) against a Standard Schema
or custom validator function.

As requested in the original issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@productdevbook productdevbook requested a review from pi0 as a code owner March 15, 2026 07:42
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

The PR adds getPayload and getValidatedPayload utilities that merge route parameters, request body, and query parameters based on HTTP method. New functions are exported from the index and accompanied by comprehensive unit tests covering different HTTP methods and validation scenarios.

Changes

Cohort / File(s) Summary
Payload Utilities
src/utils/payload.ts
Introduces getPayload for merging route params with body (POST/PUT/PATCH/DELETE) or query (GET/HEAD). Adds getValidatedPayload with overloads for StandardSchemaV1 and custom validators, delegating validation to validateData.
Public API Exports
src/index.ts
Re-exports getPayload and getValidatedPayload under Payload section.
Tests
test/unit/package.test.ts, test/unit/payload.test.ts
Verifies new functions are exported and adds comprehensive unit test suite covering payload merging, priority handling (body/query override route params), validation with Zod schema, and error responses.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A fluffy assistant once hopped through the code,
With payloads in pockets and params in tow,
Query and body merged neat in one place,
Validation so swift with StandardSchema grace,
Now HTTP requests find comfort and ease!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding two new utility functions getPayload and getValidatedPayload to the codebase.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #785: getPayload merges route params with query/body based on HTTP method, getValidatedPayload provides schema validation, both utilities are exported, and comprehensive tests validate all behaviors.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #785: new payload utilities in src/utils/payload.ts, re-exports in src/index.ts, and tests in test/unit/payload.test.ts with no unrelated modifications.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/get-payload
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 15, 2026

Open in StackBlitz

npm i https://pkg.pr.new/h3@1339

commit: 358918e

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

🧹 Nitpick comments (1)
test/unit/payload.test.ts (1)

7-85: Add explicit HEAD and DELETE cases to lock method-branch behavior.

Current coverage validates GET/POST/PUT paths well, but dedicated tests for HEAD (query branch) and DELETE (body branch) would better protect the new method dispatch logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/payload.test.ts` around lines 7 - 85, Add two tests to explicitly
cover HEAD (use getPayload) and DELETE (use getPayload/getValidatedPayload as
appropriate) to lock method-branch behavior: create a HEAD route (e.g.,
t.app.head("/search", async (event) => getPayload(event))) and fetch
"/search?q=test" with method "HEAD", asserting the returned payload contains the
query param; create a DELETE route (e.g., t.app.delete("/items/:id", async
(event) => getPayload(event))) and fetch "/items/123" with method "DELETE" and a
JSON body, asserting the returned payload includes the body fields (and that
body overrides route params on conflict), and similarly add a DELETE test for
getValidatedPayload that returns 400 on invalid body to mirror the POST
validation test.
🤖 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/utils/payload.ts`:
- Around line 38-39: The current return uses (await readBody(event)) || {} which
drops valid falsy parsed bodies (0, false, ""), so change the check to only
default when body is strictly undefined (e.g., const body = await
readBody(event); const safeBody = body === undefined ? {} : body) and then merge
using params and when safeBody is an object (typeof safeBody === "object" &&
safeBody !== null) spread it, otherwise include it as { body: safeBody }; update
references to readBody, body, params and the return expression accordingly.
- Around line 61-67: The custom-validator overloads (getValidatedPayload,
readValidatedBody, getValidatedQuery, getValidatedRouterParams) declare
options.onError as a zero-arg callback but the implementation calls it with a
FailureResult; update those overload signatures to use the OnValidateError type
(i.e., options?: { onError?: OnValidateError }) to match the StandardSchema
overloads and the actual call site. Ensure the onError param type accepts the
FailureResult (or the existing FailureResult type alias) so callers can inspect
validation. Adjust any related type imports/exports (OnValidateError,
FailureResult) so the overloads and implementation are consistent for
ValidateResult/ValidateResult<OutputT> usage.

---

Nitpick comments:
In `@test/unit/payload.test.ts`:
- Around line 7-85: Add two tests to explicitly cover HEAD (use getPayload) and
DELETE (use getPayload/getValidatedPayload as appropriate) to lock method-branch
behavior: create a HEAD route (e.g., t.app.head("/search", async (event) =>
getPayload(event))) and fetch "/search?q=test" with method "HEAD", asserting the
returned payload contains the query param; create a DELETE route (e.g.,
t.app.delete("/items/:id", async (event) => getPayload(event))) and fetch
"/items/123" with method "DELETE" and a JSON body, asserting the returned
payload includes the body fields (and that body overrides route params on
conflict), and similarly add a DELETE test for getValidatedPayload that returns
400 on invalid body to mirror the POST validation test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9eb2d4d5-4484-45e2-8767-9e266494afe2

📥 Commits

Reviewing files that changed from the base of the PR and between 10bc7ce and 358918e.

📒 Files selected for processing (4)
  • src/index.ts
  • src/utils/payload.ts
  • test/unit/package.test.ts
  • test/unit/payload.test.ts

Comment thread src/utils/payload.ts
Comment on lines +38 to +39
const body = (await readBody(event)) || {};
return { ...params, ...(typeof body === "object" ? body : { body }) } as T;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve falsy JSON primitive bodies instead of dropping them.

Line 38 uses || {}, so valid parsed bodies like 0, false, and "" are treated as empty and lost.

Suggested fix
-    const body = (await readBody(event)) || {};
-    return { ...params, ...(typeof body === "object" ? body : { body }) } as T;
+    const body = await readBody(event);
+    return {
+      ...params,
+      ...(body === undefined
+        ? {}
+        : body !== null && typeof body === "object"
+          ? body
+          : { body }),
+    } as T;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/payload.ts` around lines 38 - 39, The current return uses (await
readBody(event)) || {} which drops valid falsy parsed bodies (0, false, ""), so
change the check to only default when body is strictly undefined (e.g., const
body = await readBody(event); const safeBody = body === undefined ? {} : body)
and then merge using params and when safeBody is an object (typeof safeBody ===
"object" && safeBody !== null) spread it, otherwise include it as { body:
safeBody }; update references to readBody, body, params and the return
expression accordingly.

Comment thread src/utils/payload.ts
Comment on lines +61 to +67
export function getValidatedPayload<Event extends HTTPEvent, OutputT>(
event: Event,
validate: (
data: Record<string, unknown>,
) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>,
options?: { onError?: () => ErrorDetails },
): Promise<OutputT>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify onError callback signatures across validated helpers.
rg -nP --type=ts -C3 'export function getValidated(Payload|Query|Body)\(' src/utils
rg -nP --type=ts -C3 'onError\?:' src/utils/payload.ts src/utils/request.ts src/utils/body.ts src/utils/internal/validate.ts

Repository: h3js/h3

Length of output: 7268


🏁 Script executed:

rg -nP --type=ts 'type OnValidateError' src/
rg -nP --type=ts -A5 'onError\(' src/utils/internal/validate.ts

Repository: h3js/h3

Length of output: 1654


🏁 Script executed:

sed -n '10,20p' src/utils/internal/validate.ts

Repository: h3js/h3

Length of output: 360


Custom-validator overloads have inconsistent onError signatures across multiple functions.

The custom validator overloads narrow onError to a zero-arg callback, but the implementation always invokes it with a FailureResult object. This typing mismatch breaks type inference for accessing validation issues.

Affected overloads:

  • getValidatedPayload (line 66)
  • readValidatedBody (line 62)
  • getValidatedQuery (line 70)
  • getValidatedRouterParams (line 174)

Change custom validator overloads to match OnValidateError type and StandardSchema counterparts:

Example fix for getValidatedPayload
 export function getValidatedPayload<Event extends HTTPEvent, OutputT>(
   event: Event,
   validate: (
     data: Record<string, unknown>,
   ) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>,
-  options?: { onError?: () => ErrorDetails },
+  options?: { onError?: (result: FailureResult) => ErrorDetails },
 ): Promise<OutputT>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getValidatedPayload<Event extends HTTPEvent, OutputT>(
event: Event,
validate: (
data: Record<string, unknown>,
) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>,
options?: { onError?: () => ErrorDetails },
): Promise<OutputT>;
export function getValidatedPayload<Event extends HTTPEvent, OutputT>(
event: Event,
validate: (
data: Record<string, unknown>,
) => ValidateResult<OutputT> | Promise<ValidateResult<OutputT>>,
options?: { onError?: (result: FailureResult) => ErrorDetails },
): Promise<OutputT>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/payload.ts` around lines 61 - 67, The custom-validator overloads
(getValidatedPayload, readValidatedBody, getValidatedQuery,
getValidatedRouterParams) declare options.onError as a zero-arg callback but the
implementation calls it with a FailureResult; update those overload signatures
to use the OnValidateError type (i.e., options?: { onError?: OnValidateError })
to match the StandardSchema overloads and the actual call site. Ensure the
onError param type accepts the FailureResult (or the existing FailureResult type
alias) so callers can inspect validation. Adjust any related type
imports/exports (OnValidateError, FailureResult) so the overloads and
implementation are consistent for ValidateResult/ValidateResult<OutputT> usage.

Comment thread src/utils/payload.ts
const params = getRouterParams(event, opts);
if (_payloadMethods.has(event.req.method)) {
const body = (await readBody(event)) || {};
return { ...params, ...(typeof body === "object" ? body : { body }) } as T;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need an object with null proto

Comment thread src/utils/payload.ts
const body = (await readBody(event)) || {};
return { ...params, ...(typeof body === "object" ? body : { body }) } as T;
}
const query = getQuery(event);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can use event.url to get query instead of getQuery

Comment thread src/utils/payload.ts
event: H3Event | HTTPEvent,
opts?: { decode?: boolean },
): Promise<T> {
const params = getRouterParams(event, opts);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

router params should not be included. only query+body

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

in my original issue #785, I did include route params. This way has been most useful for me. Any downsides?

Comment thread src/utils/payload.ts
): Promise<T> {
const params = getRouterParams(event, opts);
if (_payloadMethods.has(event.req.method)) {
const body = (await readBody(event)) || {};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should use event.req.json()

@pi0 pi0 marked this pull request as draft March 15, 2026 18:27
Copy link
Copy Markdown

@danielkellyio danielkellyio left a comment

Choose a reason for hiding this comment

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

just a couple comments based on my usage of a similar project specific utility. Thanks!

Comment thread src/utils/payload.ts
event: H3Event | HTTPEvent,
opts?: { decode?: boolean },
): Promise<T> {
const params = getRouterParams(event, opts);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

in my original issue #785, I did include route params. This way has been most useful for me. Any downsides?

Comment thread src/utils/payload.ts
* });
*
* @example
* app.get("/search/:category", async (event) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's also been useful in my experience to include query params for post requests, so that order of importance goes:

  1. body
  2. query params
  3. route params

For example a post request to:
/users/:id?email=test@test.com

with the body:
{ name: "Alice" }

would result in:

const { id, name, email } = await getPayload(event);

Why?
Mostly just when I'm moving fast and mix up query and body. But the idea being the payload is just everything that we pass as data to the backend

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getPayload helper

3 participants