From 0532fc6fd61c71455ffa4d2af3cf898184e15b3f Mon Sep 17 00:00:00 2001 From: sucloudflare Date: Tue, 26 May 2026 08:35:05 -0300 Subject: [PATCH] fix: allow route handlers to return plain objects as JSON responses Closes #30 When a route returns a plain object (e.g. `return { ok: true }`) instead of `return new WinterSpecJsonResponse({ ok: true })`, the runtime now automatically wraps it in a WinterSpecJsonResponse and serializes it, including validation against the routeSpec.jsonResponse schema when present. Tests: 3 new e2e tests in tests/endpoints/plain-object-return.test.ts --- src/create-with-winter-spec.ts | 12 ++++ tests/endpoints/plain-object-return.test.ts | 77 +++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 tests/endpoints/plain-object-return.test.ts diff --git a/src/create-with-winter-spec.ts b/src/create-with-winter-spec.ts index 229a28c..9ebba2f 100644 --- a/src/create-with-winter-spec.ts +++ b/src/create-with-winter-spec.ts @@ -198,6 +198,18 @@ function serializeToResponse( throw new Error("Unknown Response type") } + // Allow routes to return a plain object instead of WinterSpecJsonResponse. + // e.g. `return { ok: true }` is equivalent to `return new WinterSpecJsonResponse({ ok: true })`. + if ( + response !== null && + typeof response === "object" && + !(response instanceof Response) + ) { + return new WinterSpecJsonResponse(response as any).serializeToResponse( + routeSpec.jsonResponse ?? z.any() + ) + } + return response } diff --git a/tests/endpoints/plain-object-return.test.ts b/tests/endpoints/plain-object-return.test.ts new file mode 100644 index 0000000..7cc2139 --- /dev/null +++ b/tests/endpoints/plain-object-return.test.ts @@ -0,0 +1,77 @@ +import test from "ava" +import { z } from "zod" +import { getTestRoute } from "../fixtures/get-test-route.js" + +test("plain-object-return: GET route returning plain object is serialized as JSON", async (t) => { + const { axios } = await getTestRoute(t, { + globalSpec: { + apiName: "hello-world", + productionServerUrl: "https://example.com", + authMiddleware: {}, + beforeAuthMiddleware: [], + }, + routeSpec: { + auth: "none", + methods: ["GET"], + }, + routePath: "/health", + routeFn: async (_req: any): Promise => { + return { ok: true } + }, + }) + + const { data, status } = await axios.get("/health") + t.is(status, 200) + t.is(data.ok, true) +}) + +test("plain-object-return: POST route returning plain object with multiple fields", async (t) => { + const { axios } = await getTestRoute(t, { + globalSpec: { + apiName: "hello-world", + productionServerUrl: "https://example.com", + authMiddleware: {}, + beforeAuthMiddleware: [], + }, + routeSpec: { + auth: "none", + methods: ["POST"], + jsonResponse: z.object({ + message: z.string(), + count: z.number(), + }), + }, + routePath: "/items", + routeFn: async (_req: any): Promise => { + return { message: "created", count: 42 } + }, + }) + + const { data, status } = await axios.post("/items") + t.is(status, 200) + t.is(data.message, "created") + t.is(data.count, 42) +}) + +test("plain-object-return: plain object validates against jsonResponse schema", async (t) => { + const { axios } = await getTestRoute(t, { + globalSpec: { + apiName: "hello-world", + productionServerUrl: "https://example.com", + authMiddleware: {}, + beforeAuthMiddleware: [], + }, + routeSpec: { + auth: "none", + methods: ["GET"], + jsonResponse: z.object({ ok: z.boolean() }), + }, + routePath: "/check", + routeFn: async (_req: any): Promise => { + return { ok: true } + }, + }) + + const { data } = await axios.get("/check") + t.is(data.ok, true) +})