End-to-end Typing for Next.js API
Tags: nextjs, typescript, react, structure
Published: 2022-08-20 23:41:00

One thing I realized after writing TypeScript (TS) for some while is "we write TS so we can keep writing JS". I used to sprinkle type annotations for the sake of typings but that's not how it works. What I should do is, write TS in one place, and put a structure so I can infer the type from where I need it without having to manage more types.

To see the difference, here's how I would write an API handler before and after applying the new structure:

![greeting.old.ts vs greeting.ts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9qxzkrs7mmur8yjuw5h8.png)

This post is my attempt to extract what I wish I knew when I started. The TS knowledge is applicable anywhere you use TS, but I'll use Next.js to present the idea.

> Before going further, step 1 to 2 are pretty basic if you're familiar with TS – so feel free to skim. The typing progress starts from step 3.

## 1. Setting up the stage

Let's set up the stage by creating a Next.js repo that uses TS out of the box:

```sh
npx create-next-app@latest --ts
```

Once done, you'll have these files:
![List of Next.js files](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v9wb6e39dp6onbacz6qk.png)

> At the time of this writing, I'm using Next.js 12

To ensure Next.js is ready, run `yarn dev` and `curl` the `api/hello` endpoint to see its response. When you stop your `yarn dev` terminal session (use `ctrl+c`), the `curl` should no longer work.

![GIF for yarn dev](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nusb6o94ii2dabz0ab0k.gif) 

Now, let's install more packages (we'll explains their uses as we go):

```sh
yarn add zod http-status-code @sentry/nextjs
```

## 2. Use Absolute Import

Open the `pages/api/hello.ts` file in vscode and add this import statement, you'll see the red squiggle:

![Red squiggle in import "@backend"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/18rgwmsnf5ph5efry7mx.png)

TS tried to find `@backend` package in node_modules but it couldn't find, hence the error. We don't need to install the package because it'll be a folder that we refer to using absolute import. Let's set it up:

1. Open your `tsconfig.json` and add these lines below `compilerOptions`:

```js
"baseUrl": ".",
"paths": {
  "@api/*": [
    "pages/api/*"
  ],
  "@backend": [
    "backend"
  ],
  "@backend/*": [
    "backend/*"
  ]
},
```

![Content of tsconfig.json](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j9vq7mxsd8lagq3mda1k.png) 

Next, let's create a new folder `backend` and create `index.ts` file in it:

![Content of backend/index.ts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bjnbmd56y9fucbpao0ag.png)
 
Open the `pages/api/hello.ts` file again and the red squiggle is now gone!

![Red squiggle removed from api/hello.ts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/unkqlke7odjkckzj0yon.png)

Based on the newly added `baseUrl` and `paths` in our `tsconfig.json`, TS knows which folder to find when it sees `"@backend"`. We call this set up "absolute import". Using absolute import is easier compared to relative import where we have to use `../` or `../../` to access files in parent folders.

> 1) Since we've added `"baseUrl":"."` and the `backend` folder is actually in the root directory of the project, we can do `import {} "backend"` and it'll still work. We'll stick with the `"@backend"` because (in my opinion) the `@` prefix is helpful to hint that it's an absolute import.

> 2) We also added absolute path for `"@api/*"` which we'll get to it later.

## 3. Add files to backend folder
Open this [Github gist](https://gist.github.com/wzulfikar/9894181bb3463128bcba2d5cc91ef94b) and copy the content to its corresponding file in your `backend` folder:

![Github gist for backend content](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vtgrsdnhcbwpyjpnphgd.png)

Your backend folder should look like this:

![Content of backend folder after adding gist](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wvsahdu9hcracvdy0byv.png)

Once all in place, let's run a type check to ensure there's no error:

```sh
yarn tsc --noEmit
```

![Run typescript check](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eyly53fl7wahyjcn2vic.png)

> We use `--noEmit` to tell TS to run typechecking without actually compiling the files to JS.
 
## 4. Let's see the types!

Open the `pages/api/hello.ts` file and notice that Next.js has added a type `Data` for the JSON response. If you pass a wrong shape for the parameter, TS will complain:

![Type error in hello.ts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1jtciq3m65fje3fw6gqq.png) 

Try save the file while having the red squiggly line and run a type check (`yarn tsc --noEmit`):

![tsc with error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4xt7ds66pi2i9a54s5o7.png)

You see that the type check didn't pass because there's an error. This is one way of using TS to prevent accidental bug to creep in to production. For example, we can run the type check automatically (eg. using Github Action) for each commit and prevent the commit to get merged to `main` if the check is not passing.

Now we know that Next.js has added the type for the response data. But what if we want to type the request too? Open [this gist](https://gist.github.com/wzulfikar/9fbe4ef13429e0f29581e611bdc30251) and copy the content to `pages/api/greeting.ts`:

![pages/api/greeting.ts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8l36w52wf6tna3mto4ud.png)
 
Here's how we read above codes:
- `Line 1`: we import the type `ApiHandler` and the functions (`handle`, `z`) from backend folder (see `backend/index.ts` file from step 3). The `z` actually comes from `zod` package but we re-export it thru backend so we don't have to add multiple imports. This is just a convenient approach because for our purpose, `z` will almost always get imported when `handle` is.
- `Line 3-7`: define `schema` variable that we'll use to validate the request and add the typing to our request object (`req`). It tells us which properties (in query or body) are available for this endpoint.
- `Line 9-11`: define `response` to type the shape of our response object (`res`). In this case, it has single property called `greeting` (a string). Unlike `schema`, `response` is exported because we want to reuse it in our React component later.
- `Line 13-16`: define `handler` function which is the meat of our API code. We use `ApiHandler`, a generic we defined in `backend/types.ts:25`, to add types to our request and response objects based on the type of `schema` and `response`.

    ![type inference](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pzrj0jdzc8142xmrsp44.gif)

- `Line 18`: pass the `handler` to our `handle` function which will automatically validate the request against the schema. It guarantees that all properties defined in `schema`  will be available in `handler`. For example, it'll drop the request and return an error response if user doesn't provide `name` in query param. This way, our `handler` doesn't have to deal with manual validation (eg. checking if `name` is not empty).

    ![curl with query](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iqlwivmet0vnpljhrzhf.gif)

**There we go!** We now have a structure to type our API. I like the way it starts with declarative style (declare the shape of schema and response) and continues with imperative style (the handler). 

When we have multiple files of similar structure, it'll be easy to skim because there's a pattern: shape of schema, shape of response, the handler. The handler is pretty slim too because it doesn't need to care about data validation.

For the next part, we'll see how we reuse the `response` to add typing in our React component. We'll also add a structure to test both the backend and frontend using `jest`. Here's a sneak peek:

![yarn test backend frontend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7w11q84m5jxkpf6m4n49.gif)

Stay tuned!