简体   繁体   中英

How to add TypeScript types to request body in Next.js API route?

Problem

One of the primary reasons I like using TypeScript is to check that I am passing correctly typed params to given function calls.

However, when using Next.js, I am running into the issue where params passed to a Next.js API endpoint end up losing their types when the are "demoted" to the NextApiRequest type.

Typically, I would pull off params doing something like req.body.[param_name] but that entire chain has type any so I lose any meaningful type information.

Example

Let's assume I have an API endpoint in a Next.js project at pages/api/add.ts which is responsible for adding two numbers. In this file, we also have a typed function for adding two numbers, that the API endpoint will call.

const add = (a: number, b: number): number => a + b;

export default async (req: NextApiRequest, res: NextApiResponse) => {
  try {
    const result = add(req.body.number_one, req.body.number_two);
    res.status(200).json(result);
  } catch (err) {
    res.status(403).json({ err: "Error!" });
  }
};

The problem I am running into, and what I would like help with, is how to type req.body.number_one and req.body.number_two or any sort of param coming off of the request object. Is this possible?

Since the types off of the request object are any TypeScript does not complain, even if you try calling the API endpoint with incorrectly typed params.

fetch("/api/add", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ number_one: 1, number_two: "two" }),
});

The TypeScript compiler will have no problems with the above call to the API endpoint, even though the types are incorrect. It treats the body as an any type, so it loses any type information.

It would be great if I could type params cast from the request body being sent to API endpoints in Next.js

You can create a new interface that extends NextApiRequest and adds the typings for the two fields.

interface ExtendedNextApiRequest extends NextApiRequest {
  body: {
    number_one: number;
    number_two: number;
  };
}

Then use it to type the req object in the handler function.

export default async (req: ExtendedNextApiRequest, res: NextApiResponse) => {
    //...
};

While extending the NextApiRequest type will stop TypeScript from complaining, it does not prevent potential runtime errors from occurring.

For a better, type-safe approach that narrows down the types instead, check out @Matthieu Gellé's answer .

julio's answer works but it is not encouraged by the official documentation :
Extending the req/res objects with TypeScript

const add = (a: number, b: number): number => a + b;

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const { body } = req;
  try {
    if (
      "number_one" in body &&
      typeof body.number_one === "number" &&
      "number_two" in body &&
      typeof body.number_two === "number"
    ) {
      const result = add(body.number_one, body.number_two);
      return res.status(200).json(result);
    }
    throw new Error("number_one or number_two is not a number");
  } catch (err) {
    return res.status(403).json({ err: "Error!" });
  }
};

I haven't modified your code much so that you can integrate it easily, if you have the courage to come and modify this brick despite the fact that it "works"

Note: I'm one of the maintainers of Remult

If you can live with another dependency, with Remult you use type-safe code for all your API calls (CRUD for TypeScript models and RPC for functions).

Your add function would look like this:

// utils.ts
import { BackendMethod } from "remult";

export class ApiUtils {
   @BackendMethod({ allowed: true })
   static async add(a: number, b: number) {
      return a + b;
   }
}

and you'd import it in the frontend and call it directly:

// App.tsx
import { ApiUtils } from 'utils'

alert(await ApiUtils.add(1,2));

Remult will send the POST request, handle it on the Next.js API route, and pass the values to the add function on the backend.

There's a Next.js tutorial if you want to explore further.

Just make a Type Guard and use it within your handler. Matthieu's answer is great but nasty when having many expected properties.

Checking if something sent through the body is of some type can take quite a lot of time when you find yourself going through 5+ properties. Even worse headaches if those are nested multiple levels. Just use proper validators and write schemas for them.

For this purpose, as Matthieu specified, you shouldn't extend NextApiRequest and NextApiResponse by overriding existing properties, only extend them to add additional ones.

Instead, I'd write a generic like this:

function isValidBody<T extends Record<string, unknown>>(
  body: any,
  fields: (keyof T)[],
): body is T {
  return Object.keys(body).every((key) => fields.includes(key))
}

And use it like this:

type RequestBody = {
  id: string
}

async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (!isValidBody<RequestBody>(req.body, ['id'])) {
    return res.status(402)
  }

  // req.body.id - is expected to be a string down the road
}

Reference: Using type predicates

Coming in a bit late, but I had a similar issue for the request query params. I got this working and I believe it's still type safe. Just adds some auto-complete and code-level documentation to intended available parameters: https://github.com/vercel/next.js/discussions/36373

Should be easily modified to support body as well

Had this issue as well! Ended up defining the body object and passing it into fetch with an interface for for what the req body in the api

In the case of your example --

pages/api/add.ts

    export interface addBody {
       number_one:number;
       number_two:number
    }

    export default async (req: NextApiRequest, res: NextApiResponse) => {
      try {
        const result = add(req.body.number_one, req.body.number_two);
        res.status(200).json(result);
      } catch (err) {
        res.status(403).json({ err: "Error!" });
      }
    };

and for where you call fetch --

import { addBody } from "pages/api/add.ts";

    const body:addBody = { number_one: 1, number_two: "two" }

    fetch("/api/add", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

Ended up working for me and typescript throws an error when I don't have all the params I'd like. Hope this helps!

I think we can use express-validator validate the body before use it.

Then a simple method can used from the Extending the req/res objects with TypeScript

type ExtendedRequest = NextApiRequest & {
  body: {
    number_one: number;
    number_two: number;
  };
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM