How to encode query param for a tRPC query using fetch

3.5k Views Asked by At

According to tRPCs documentation, the query params have to follow this format

myQuery?input=${encodeURIComponent(JSON.stringify(input))}

I have this procedure:

  hello: publicProcedure
    .input(z.object({ text: z.string() }))
    .output(z.object({ greeting: z.string() }))
    .query(({ input }) => {
      return {
        greeting: `Hello ${input.text}`,
      };
    }),

A manually constructed URL returns an error:

const data = {text: "my message"}
const res = await fetch('http://localhost:3000/api/trpc/example.hello?batch=1&input='+encodeURIComponent(JSON.stringify(data)), { method: 'GET' });
const body = await res.json();
console.log(body);

The error indicates that the query params are not encoded correctly? Any idea what's going wrong? Using the client, it works: const test = api.example.hello.useQuery({ text: "my message" });

{
    "error": {
        "json": {
            "message": "[\n  {\n    \"code\": \"invalid_type\",\n    \"expected\": \"object\",\n    \"received\": \"undefined\",\n    \"path\": [],\n    \"message\": \"Required\"\n  }\n]",
            "code": -32600,
            "data": {
                "code": "BAD_REQUEST",
                "httpStatus": 400,
                "stack": "TRPCError: [\n  {\n    \"code\": \"invalid_type\",\n    \"expected\": \"object\",\n    \"received\": \"undefined\",\n    \"path\": [],\n    \"message\": \"Required\"\n  }\n]\n    at inputMiddleware (file:///Users/michael/Projects/t3/test/my-t3-app/node_modules/@trpc/server/dist/index.mjs:252:19)\n    at runMicrotasks (<anonymous>)\n    at processTicksAndRejections (node:internal/process/task_queues:96:5)\n    at async callRecursive (file:///Users/michael/Projects/t3/test/my-t3-app/node_modules/@trpc/server/dist/index.mjs:419:32)\n    at async resolve (file:///Users/michael/Projects/t3/test/my-t3-app/node_modules/@trpc/server/dist/index.mjs:447:24)\n    at async file:///Users/michael/Projects/t3/test/my-t3-app/node_modules/@trpc/server/dist/resolveHTTPResponse-a3869d43.mjs:123:32\n    at async Promise.all (index 0)\n    at async resolveHTTPResponse (file:///Users/michael/Projects/t3/test/my-t3-app/node_modules/@trpc/server/dist/resolveHTTPResponse-a3869d43.mjs:120:28)\n    at async nodeHTTPRequestHandler (file:///Users/michael/Projects/t3/test/my-t3-app/node_modules/@trpc/server/dist/nodeHTTPRequestHandler-e46cee59.mjs:51:20)\n    at async file:///Users/michael/Projects/t3/test/my-t3-app/node_modules/@trpc/server/dist/adapters/next.mjs:40:9\n    at async Object.apiResolver (/Users/michael/Projects/t3/test/my-t3-app/node_modules/next/dist/server/api-utils/node.js:363:9)\n    at async DevServer.runApi (/Users/michael/Projects/t3/test/my-t3-app/node_modules/next/dist/server/next-server.js:487:9)\n    at async Object.fn (/Users/michael/Projects/t3/test/my-t3-app/node_modules/next/dist/server/next-server.js:749:37)\n    at async Router.execute (/Users/michael/Projects/t3/test/my-t3-app/node_modules/next/dist/server/router.js:253:36)\n    at async DevServer.run (/Users/michael/Projects/t3/test/my-t3-app/node_modules/next/dist/server/base-server.js:384:29)\n    at async DevServer.run (/Users/michael/Projects/t3/test/my-t3-app/node_modules/next/dist/server/dev/next-dev-server.js:741:20)\n    at async DevServer.handleRequest (/Users/michael/Projects/t3/test/my-t3-app/node_modules/next/dist/server/base-server.js:322:20)",
                "path": "example.hello"
            }
        }
    }
}

I inspected the query sent by my browser when using the client (const test = api.example.hello.useQuery({ text: "my message" });). The query below, and it succeeds.

http://localhost:3000/api/trpc/example.hello?batch=1&input=%7B%220%22%3A%7B%22json%22%3A%7B%22text%22%3A%22my%20message%22%7D%7D%7D

If I decode the input query parameter, I see {"0":{"json":{"text":"my message"}}}

If I encode my construct my data object the same way, the query still fails:

const data = {"0":{"json":{"text":"my message"}}}
const res = await fetch('http://localhost:3000/api/trpc/example.hello?batch=1&input='+encodeURIComponent(JSON.stringify(data)), { method: 'GET' });
const body = await res.json();
console.log(body);

The 0 seems to be necessary b/c batching is enabled? But the json field seems odd.

{"0":{"json":{"text":"my message"}}}

Any idea why my constructed fetch fails? What's the right format of the encoding/ structure of the object?

2

There are 2 best solutions below

2
On

It's not possible to tell you the exact structure your request needs to have as this depends on how you set up tRPC, in particular your links and transformer. I also can't tell if your request is structured correctly because you didn't show what is inside of data for each request.

That being said, let's look at it in a more general way.

The json key that is required in your request is almost certainly because you have set superjson set as your transformer. This is the standard in most templates that use tRPC, such as Create T3 App or the example projects in tRPC's GitHub, so unless you scaffold your app from scratch you'll probably have this enabled. The purpose of superjson is to make it easier to send some data types that are tricky with regular JSON, such as Dates.

Regarding batch, everything you wrote is correct. However you can also modify this behavior. It's determined by the ending link. If you don't want to batch, you can use httpLink instead of httpBatchLink (but in most cases the batch link is recommended).

Now let's look at some sample requests (I have them not URI encoded here to make them easier to read)

superjson, httpBatchLink:

http://localhost:3000/api/trpc/example.hello?batch=1&input={"0":{"json":{"text":"from tRPC"}}}

superjson, httpLink:

http://localhost:3000/api/trpc/example.hello?input={"json":{"text":"from tRPC"}}

no superjson, httpBatchLink:

http://localhost:3000/api/trpc/example.hello?batch=1&input={"0":{"text":"from tRPC"}}

no superjson, httpLink:

http://localhost:3000/api/trpc/example.hello?input={"text":"from tRPC"}

If you want to communicate with your tRPC API in a more "standard REST" way, there is trpc-openapi.

2
On

When you use superjson do not forget to add transformer as superjson when you init tRPC client.