ts-rest / ts-rest

RPC-like client, contract, and server implementation for a pure REST API
https://ts-rest.com
MIT License
2.11k stars 91 forks source link

createNextRouter with a contentType multipart/form-data contract will always fail validation #604

Closed ChiefORZ closed 1 month ago

ChiefORZ commented 1 month ago

Describe the bug

We tried to create a contentType multipart/form-data contract:

const documentContract = c.router({
  createDocument: {
    body: createDocumentInputSchema,
    contentType: "multipart/form-data",
    method: "POST",
    path: "/documents",
    responses: {
      201: documentSchema,
    },
    summary: "Create document resource",
  },
});

In the request itself we are creating a new FormData instance that we pass to the fetch:

export const documentController = initQueryClient(documentContract, {
  baseUrl: "/api",
});

const documentMutation = documentController.createDocument.useMutation();

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  const formData = new FormData(e.currentTarget);
  const title = formData.get("title");
  const file = formData.get("file");
  await documentMutation.mutateAsync({
    body: formData,
  });
};

The request is done and the content-type headers are set correctly and the multipart/form-data body looks perfectly find, still the next api resolves in an error:

{
  "issues": [
    {
      "code": "invalid_type",
      "expected": "object",
      "received": "string",
      "path": [],
      "message": "Expected object, received string"
    }
  ],
  "name": "ZodError"
}

How to reproduce

  1. git clone https://github.com/ChiefORZ/nextjs-ts-rest-headers-bug.git
  2. cd nextjs-ts-rest-headers-bug
  3. pnpm dev
  4. open http://localhost:3000
  5. submit a title and a random file in the form

-> POST /document response will fail with

{
  "issues": [
    {
      "code": "invalid_type",
      "expected": "object",
      "received": "string",
      "path": [],
      "message": "Expected object, received string"
    }
  ],
  "name": "ZodError"
}

Expected behavior

No response

Code reproduction

https://github.com/ChiefORZ/nextjs-ts-rest-headers-bug

ts-rest version

3.45.2

Gabrola commented 1 month ago

Next.js's body parser does not support multipart/form-data. Most frameworks do not out of the box either, so you need to use something like multer to correctly parse the body as outlined here in the case of express.js.

Because, next.js route handlers do not have middleware, you'll need to define your own handler for that specific endpoint, call multer, then forward the request to the handler defined by createSingleRouteHandler.

import multer from 'multer';

export const config = {
  api: {
    bodyParser: false,
  },
};

const upload = multer();

export default async (req: NextApiRequest, res: NextApiResponse) => {
  upload.none()(req, null, (error) => {
    // req.body will now have the text fields
    return createSingleRouteHandler(contract.endpoint, async (args) => ...)(req, res);
  });
};