Skip to content

Commit 9070052

Browse files
feat: add getPayload utility for unified request data access
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>
1 parent 10bc7ce commit 9070052

File tree

4 files changed

+105
-0
lines changed

4 files changed

+105
-0
lines changed

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ export {
118118

119119
export { readBody, readValidatedBody, assertBodySize } from "./utils/body.ts";
120120

121+
// Payload
122+
123+
export { getPayload } from "./utils/payload.ts";
124+
121125
// Cookie
122126

123127
export {

src/utils/payload.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { H3Event, HTTPEvent } from "../event.ts";
2+
import { getQuery } from "./request.ts";
3+
import { readBody } from "./body.ts";
4+
import { getRouterParams } from "./request.ts";
5+
6+
const _payloadMethods = new Set(["PATCH", "POST", "PUT", "DELETE"]);
7+
8+
/**
9+
* Get the request payload by merging route params, query params, and body data.
10+
*
11+
* For `GET` and `HEAD` requests, returns query params merged with route params.
12+
* For `POST`, `PUT`, `PATCH`, and `DELETE` requests, returns parsed body merged with route params.
13+
*
14+
* Route params take lowest priority (body/query overrides them).
15+
*
16+
* @example
17+
* app.post("/users/:id", async (event) => {
18+
* const payload = await getPayload(event);
19+
* // { id: "123", name: "Alice" } — id from route, name from body
20+
* });
21+
*
22+
* @example
23+
* app.get("/search/:category", async (event) => {
24+
* const payload = await getPayload(event);
25+
* // { category: "books", q: "h3" } — category from route, q from query
26+
* });
27+
*/
28+
export async function getPayload<T = Record<string, unknown>>(
29+
event: H3Event | HTTPEvent,
30+
opts?: { decode?: boolean },
31+
): Promise<T> {
32+
const params = getRouterParams(event, opts);
33+
if (_payloadMethods.has(event.req.method)) {
34+
const body = (await readBody(event)) || {};
35+
return { ...params, ...(typeof body === "object" ? body : { body }) } as T;
36+
}
37+
const query = getQuery(event);
38+
return { ...params, ...query } as T;
39+
}

test/unit/package.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe("h3 package", () => {
5858
"getHeader",
5959
"getHeaders",
6060
"getMethod",
61+
"getPayload",
6162
"getProxyRequestHeaders",
6263
"getQuery",
6364
"getRequestFingerprint",

test/unit/payload.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, it, expect } from "vitest";
2+
import { H3, getPayload } from "../../src/index.ts";
3+
import { describeMatrix } from "../_setup.ts";
4+
5+
describeMatrix("getPayload", (t, { it, expect }) => {
6+
it("returns query params for GET requests", async () => {
7+
t.app.get("/search", async (event) => {
8+
return getPayload(event);
9+
});
10+
const res = await t.fetch("/search?q=hello&page=1");
11+
expect(await res.json()).toMatchObject({ q: "hello", page: "1" });
12+
});
13+
14+
it("returns body for POST requests", async () => {
15+
t.app.post("/users", async (event) => {
16+
return getPayload(event);
17+
});
18+
const res = await t.fetch("/users", {
19+
method: "POST",
20+
headers: { "content-type": "application/json" },
21+
body: JSON.stringify({ name: "Alice" }),
22+
});
23+
expect(await res.json()).toMatchObject({ name: "Alice" });
24+
});
25+
26+
it("merges route params with query for GET", async () => {
27+
t.app.get("/search/:category", async (event) => {
28+
return getPayload(event);
29+
});
30+
const res = await t.fetch("/search/books?q=h3");
31+
const data = await res.json();
32+
expect(data.category).toBe("books");
33+
expect(data.q).toBe("h3");
34+
});
35+
36+
it("merges route params with body for POST", async () => {
37+
t.app.post("/users/:id", async (event) => {
38+
return getPayload(event);
39+
});
40+
const res = await t.fetch("/users/123", {
41+
method: "POST",
42+
headers: { "content-type": "application/json" },
43+
body: JSON.stringify({ name: "Bob" }),
44+
});
45+
const data = await res.json();
46+
expect(data.id).toBe("123");
47+
expect(data.name).toBe("Bob");
48+
});
49+
50+
it("body overrides route params on conflict", async () => {
51+
t.app.put("/items/:id", async (event) => {
52+
return getPayload(event);
53+
});
54+
const res = await t.fetch("/items/old", {
55+
method: "PUT",
56+
headers: { "content-type": "application/json" },
57+
body: JSON.stringify({ id: "new" }),
58+
});
59+
expect((await res.json()).id).toBe("new");
60+
});
61+
});

0 commit comments

Comments
 (0)