Have strong expressions.

Panaxeo
Plain Text by Panaxeo
2 min readMay 23, 2023

In Node.js. A Frontend techtip.

Who wouldn’t want to use strongly typed TypeScript code in their Node.js Express app? Turns out it might be a little tricky, especially when trying to define routes or request and response objects.

But worry not, there is of course a way!

Welcome to Panaxeo Tech Tips. Our monthly section with tips from our Club-Frontend members and more!

It would be easiest to extend the Express.Request and Express.Response types, right? Sure, but you might not get proper API responses regarding TypeDoc documentation — mostly because of method chaining, aka res.status(200).json() — this would use the original json() function.

What you could do is create a new type, like this:

export type TypedResponse<T> = Omit<Response, 'json' | 'status'> & { json(data: T): TypedResponse<T>, status(code: number): TypedResponse<T> }

The omit part removes the original implementation of json and status methods and replaces it with your typed version.

Afterwards, the usage is simple:

const authenticate = (req: Request, res: TypedResponse<AuthResponse | IBaseResponse>, next: NextFunction) => {
authService.authenticate(req.body)
.then(user => user
? res.json({ success: true, ...user })
: res.status(401).json({ success: false, message: 'Incorrect credentials provided' }))
.catch(err => next(err))
}

These are the models we’ve used:

// our models
export interface AuthResponse extends IBaseResponse {
name: string
email: string
token: string
}

export interface IBaseResponse {
success: boolean
message?: string
}

That’s a typed response with all the props on our T type. Yippie!

Now, what about the request types? This is a little trickier, as express uses its types (the Query, to be precise) from a package express-static-serve-core, which the express itself doesn’t expose, so we need this package to be able to extend our requests with our own Types.

It can be achieved like this:

import { Query } from 'express-serve-static-core'

export interface TypedRequest<T extends Query, U> extends Express.Request {
body: U
query: T
}

And now that we have both the body AND the query strongly typed, the usage is pretty straightforward:

const updateUserTokenData = (req: TypedRequest<{ id: string}, { token: string, subId: string }>, res: TypedResponse<IBaseResponse>, next: NextFunction) => {
userService.updateUserTokenData(req.query.id, { ...req.body })
.then(updatedToken => updatedToken
? res.json({ success: true })
: res.status(401).json({ success: false, message: "Your error reason" }))
.catch(err => next(err))
}

Both the requests and the responses are now strongly typed, our intellisense works as expected, and TypeScript will inform us if we’re not typing the correct properties.

PS: Don’t forget to validate the payload sent by the user ;)

And that’s all folks!

Found this little tip useful?
Maybe you’ve got some questions of your own, or maybe there’s a tip you’d like to share.
Let us know, our #club-frontend would love to hear it!

--

--