This post is specifically aimed at Prisma users but you should be able to use it with any ORM (or no ORM ot all if you roll that way).
One of the most annoying things about integrating different technologies together is losing static typing along the way. This is my biggest complaint when it comes to working with databases without an ORM, but is also a big issue with HTTP APIs. GraphQL and GraphQL Code Generator solve this problem beautifully, but GraphQL can still be very frustrating to work with from time to time due to its immature ecosystem. Sometimes you just want to be able to use HTTP and call it a day.
Next.JS is an amazing technology that allows us to use Typescript, but even with static typing and an ORM that has static definitions for things like a User
stored in the database, that User
is usually not the same User
you send to the frontend as API responses so often times you end up losing that static typing along the way.
Thankfully, to solve this problem, Next.js lets us share code between our frontend and the backend where we actually don’t have to lose the type information along the way.
Here’s an example of thow that’s possible:
/pages/api/user/[userId].tsTypescript
1import { NextApiRequest, NextApiResponse } from "next"2import { db } from "../your-database-file"34const response = (id: string) => {5 return db.user.findUnique({6 where: { id },7 })8}910export default async (req: NextApiRequest, res: NextApiResponse) => {11 const { userId } = req.query1213 res.json(await response(userId))14}1516export type UserResponse = PromiseReturnType<typeof response>
Here we’re querying the database with Prisma which generates type information for exactly the shape of data you’re querying for the database which is why the return value isn’t explicit in the response
function. It’s already inferred with the exact return value. There is a small issue however, which is that the type is actually Promise<Image>
and not Image
, in order to get the Image
type out, we have to unwrap the Promise type. If you’re a Prisma user, then you can simply use a type-level function it exports called PromiseReturnType
Typescript
import { PromiseReturnType } from "@prisma/client"
If not, you can use some manual Typescript magic instead.
export type PromiseReturnType<T> = ReturnType<T> extends Promise<infer R>? R: never
If you’ve never looked into the infer
keyword or conditional types, I highly recommend you do, they’re fantastic.
This way we can declare the types of the responses we get from an endpoint inside the endpoint file itself which makes importing much easier and also enables using type inference for database results from ORMs that are statically typed without repeating the typings.
All of this sounds pretty good so far, but when it comes to importing things, some of you might be wondering:
You, reading this
Hold on, am I going to have to import an API endpoint file in my frontend now? This is going to create issues importing backend code for sure.
But not to worry, as of Typescript 3.8, we can import just types from a typescript file, meaning it’s possible to do this:
/pages/home.tsxTypescript
import type { HomeResponse } from "./api/home.ts"
Making sure that you don’t accidentally bring in an entire backend bundle to your frontend when trying to share types.
This is all nice and everything, but what if we could write a function that could do the work of wrapping a function that returns a promise and replying with the response value, maybe something like:
1import { NextApiRequest, NextApiResponse } from "next"23type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>45function respond<T>(f: Handle<T>) {6 return async (req: NextApiRequest, res: NextApiResponse) => {7 try {8 const result = await f(req, res)9 res.json(result)10 } catch (err) {11 res.statusCode = 50012 res.json({ message: err })13 }14 }15}1617const handle: Handle = (req, res) => {18 return db.users.findMany({})19}2021export type UsersResponse = PromiseReturnType<typeof handle>2223export default respond(handle)
Unfortunately, this doesn’t work because of this issue about generic values. Otherwise we would be able to get the type inference working flawlessly. Typescript core team pls…