DEV Community

Cover image for End-to-end Typing for Next.js API
Wildan Zulfikar
Wildan Zulfikar

Posted on

End-to-end Typing for Next.js API

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

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:

npx create-next-app@latest --ts
Enter fullscreen mode Exit fullscreen mode

Once done, you'll have these files:
List of Next.js files

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

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

yarn add zod http-status-code @sentry/nextjs
Enter fullscreen mode Exit fullscreen mode

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"

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:
"baseUrl": ".",
"paths": {
  "@api/*": [
    "pages/api/*"
  ],
  "@backend": [
    "backend"
  ],
  "@backend/*": [
    "backend/*"
  ]
},
Enter fullscreen mode Exit fullscreen mode

Content of tsconfig.json

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

Content of backend/index.ts

Open the pages/api/hello.ts file again and the red squiggle is now gone!

Red squiggle removed from api/hello.ts

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 and copy the content to its corresponding file in your backend folder:

Github gist for backend content

Your backend folder should look like this:

Content of backend folder after adding gist

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

yarn tsc --noEmit
Enter fullscreen mode Exit fullscreen mode

Run typescript check

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

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

tsc with error

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 and copy the content to pages/api/greeting.ts:

pages/api/greeting.ts

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

  • 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

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

Stay tuned!

Oldest comments (0)