6

I'm just getting into Fastify with Typescript and really enjoying it.

However, I'm trying to figure out if I can type the response payload. I have the response schema for serialization working and that may be sufficient, but I have internally typed objects (such as IUser) that it would be nice to have Typescript check against.

The following works great, but I'd like to return an TUser for example and have typescript if I return something different. Using schema merely discludes fields.

interface IUser {
    firstname: string,
    lastname: string
} // Not in use in example

interface IUserRequest extends RequestGenericInterface {
  Params: { username: string };
}

const getUserHandler = async (
  req: FastifyRequest<IUserRequest, RawServerBase, IncomingMessage | Http2ServerRequest>
) => {
  const { username } = req.params;
  return { ... }; // Would like to return instance of IUser
};


app.get<IUserRequest>('/:username', { schema }, getUserHandler);

Is there an equivalent of RequestGenericInterface I can extend for the response?

Small Update: It seems that the reply.send() can be used to add the type, but it would be nice for self-documentation sake to provide T higher up.

Valentine Shi
  • 6,604
  • 4
  • 46
  • 46
Blaine Garrett
  • 1,286
  • 15
  • 23

4 Answers4

3

From the documentation:

Using the two interfaces, define a new API route and pass them as generics. The shorthand route methods (i.e. .get) accept a generic object RouteGenericInterface containing five named properties: Body, Querystring, Params, Headers and Reply.

You can use the Reply type.

interface MiscIPAddressRes {
  ipv4: string
}
server.get<{
  Reply: MiscIPAddressRes
}>('/misc/ip-address', async (req, res) => {
  res
    .status(_200_OKAY)
    .send({
      ipv4: req.ip // this will be typechecked
  })
})
aelesia
  • 184
  • 7
3

After looking at the type definitions, I found out that there is also an alternative way to only type-check the handler (like in Julien TASSIN's answer), like this:

import { FastifyReply, FastifyRequest, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { RouteGenericInterface } from "fastify/types/route";

interface IUser {
  firstname: string;
  lastname: string;
}

interface IUserRequest extends RouteGenericInterface {
  Params: { username: string };
  Reply: IUser; // put the response payload interface here
}

function getUserHandler(
  request: FastifyRequest<IUserRequest>,
  reply: FastifyReply<
    RawServerDefault,
    RawRequestDefaultExpression,
    RawReplyDefaultExpression,
    IUserRequest // put the request interface here
  >
) {
  const { username } = request.params;

  // do something

  // the send() parameter is now type-checked
  return reply.send({
    firstname: "James",
    lastname: "Bond",
  });
}

You can also create your own interface with generic to save writing repeating lines, like this:

import { FastifyReply, FastifyRequest, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { RouteGenericInterface } from "fastify/types/route";

export interface FastifyReplyWithPayload<Payload extends RouteGenericInterface>
  extends FastifyReply<
    RawServerDefault,
    RawRequestDefaultExpression,
    RawReplyDefaultExpression,
    Payload
  > {}

then use the interface like this:

function getUserHandler(
  request: FastifyRequest<IUserRequest>,
  reply: FastifyReplyWithPayload<IUserRequest>
) {
  const { username } = request.params;

  // do something

  // the send() parameter is also type-checked like above
  return reply.send({
    firstname: "James",
    lastname: "Bond",
  });
}
1

If you want to type the handler only, you can perform it this way

import { RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault, RouteHandler, RouteHandlerMethod } from "fastify";

const getUserHandler: RouteHandlerMethod<
    RawServerDefault,
    RawRequestDefaultExpression,
    RawReplyDefaultExpression,
    { Reply: IUser; Params: { username: string } }
> = async (
  req: FastifyRequest<IUserRequest, RawServerBase, IncomingMessage | Http2ServerRequest>
) => {
  const { username } = req.params;
  return { ... }; // Would like to return instance of IUser
};
Julien TASSIN
  • 5,004
  • 1
  • 25
  • 40
1

Trying to type these was a truely awful experience. Thanks to the other answers, this is where I ended up. Bit of a code dump to make life easier for others.

request-types.ts

With this I am standardising my response to optionally have data and message.

import {
    FastifyReply,
    FastifyRequest,
    RawReplyDefaultExpression,
    RawRequestDefaultExpression,
    RawServerDefault,
} from 'fastify';

type ById = {
    id: string;
};

type ApiRequest<Body = void, Params = void, Reply = void> = {
    Body: Body;
    Params: Params;
    Reply: { data?: Reply & ById; message?: string };
};

type ApiResponse<Body = void, Params = void, Reply = {}> = FastifyReply<
    RawServerDefault,
    RawRequestDefaultExpression,
    RawReplyDefaultExpression,
    ApiRequest<Body, Params, Reply>
>;

type RouteHandlerMethod<Body = void, Params = void, Reply = void> = (
    request: FastifyRequest<ApiRequest<Body, Params, Reply>>,
    response: ApiResponse<Body, Params, Reply>
) => void;

export type DeleteRequestHandler<ReplyPayload = ById> = RouteHandlerMethod<void, ById, ReplyPayload>;
export type GetRequestHandler<ReplyPayload> = RouteHandlerMethod<void, ById, ReplyPayload>;
export type PostRequestHandler<Payload, ReplyPayload> = RouteHandlerMethod<Payload, void, ReplyPayload>;
export type PatchRequestHandler<Payload, ReplyPayload> = RouteHandlerMethod<Payload, ById, ReplyPayload>;
export type PutRequestHandler<Payload, ReplyPayload> = RouteHandlerMethod<Payload, ById, ReplyPayload>;

Usage

get-account.ts - GetRequestHandler

export const getAccount: GetRequestHandler<AccountResponseDto> = async (request, reply) => {
    const { id } = request.params;
    ...
    const account = await Account.findOne.... 
    ...
    if (account) {
        return reply.status(200).send({ data: account });
    }

    return reply.status(404).send({ message: 'Account not found' });
};

delete-entity.ts - DeleteRequestHandler

export const deleteEntity: DeleteRequestHandler = async (request, reply) => {
    const { id } = request.params;
    ...
    // Indicate success by 200 and returning the id of the deleted entity
    return reply.status(200).send({ data: { id } });
};

update-account.ts - PatchRequestHandler

export const updateAccount: PatchRequestHandler<
    UpdateAccountRequestDto, 
    AccountResponseDto
> = async (request, reply) => {
    const { id } = request.params;
    ...
    return reply.status(200).send({ data: account });
};

register-account-routes.ts - No errors with provided handler.

export const registerAccountRoutes = (app: FastifyInstance) => {
    app.get(EndPoints.ACCOUNT_BY_ID, getAccount);
    app.patch(EndPoints.ACCOUNT_BY_ID, updateAccount);
    app.post(EndPoints.ACCOUNTS_AUTHENTICATE, authenticate);
    app.put(EndPoints.ACCOUNTS, createAccount);
};
Steve
  • 4,372
  • 26
  • 37