Skip to main content

Command Palette

Search for a command to run...

[ 살펴보기 ] Fastify - Typesciprt, Type Provider

Published
5 min readView as Markdown
[ 살펴보기 ] Fastify - Typesciprt, Type Provider
C

A developer living in Busan, Korea

Typesciprt 기반으로 fastify를 사용한다면 fastify와 더불어 typescirpt 환경에 필요한 추가 package를 설치한다.

npm install -D typescript @types/node

추가로 tsx package는 typescirpt file을 compile 과정없이 바로 실행할 수 있게 해주는 package다. 해당 package를 통해 development 환경에서 compile없이 typescirpt file을 실행하기 위해 추가로 설치한다.

npm install -D tsx

그리고 package.json script에 다음과 같이 추가해 준다.

...
"scripts": {
    "dev": "tsx watch ./src/app.ts",
    "build": "tsc",
    "start": "node ./dist/app.js",
}
...
  • dev : tsx를 통해 typescirpt file을 compile없이 node환경에서 실행하기 위한 script

  • build : typescirpt compiler를 통해 typescirpt file을 javasciprt file로 compile한다. Project folder에 tsconfig.json 파일이 있으면 tsconfig에 설정된 값에 따라 compile을 실행한다.

  • start : build command를 통해 compile된 javascript file을 node를 통해 실행한다. tsconfig를 통해 compile된 file을 dist folder에 담도록 설정을 할 것이므로 ./dist/app.js 경로로 설정한다.

그리고 typescript 설정을 위해 project root 경로에 ( project package.json파일이 존재하는 위치 ) tsconfig.json을 생성한다. 아래의 포스트는 다음과 같은 설정으로 진행한다.

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "NodeNext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["./src/**/*"]
}

테스트를 위한 project structure는 다음과 같다.

package.json
tsconfig.json
src
    app.ts
    /routes
        - member.ts

우선 app.ts 파일에 다음과 같이 기본 server 구성을 추가해보자.

import Fastify from "fastify";

const fastify = Fastify({
  logger: true,
});

const serverStart = async () => {
  try {
    await fastify.listen({ port: 3000 });
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
serverStart();

그리고 /routes 폴더에 member.ts 파일을 생성해 member route를 구성한다.

특정 route가 client request에 포함된 queryString, header를 기반으로 request를 처리한다면 queryString과 header 또한 type을 지정해 route에 적용할 수 있다.

...
type MemberRouteGet = {
  Querystring: { username: string };
  Headers: { authorization: string };
};

fastify.get<MemberRouteGet>("/", (request, reply) => {
  console.log(" ::: member get ::: ");
  const { username } = request.query;
  const { authorization } = request.headers;
  reply.send({ hello: "world 22" });
});

...

여기에 더불어 response status code에 따라 response structure type을 강제할 수도 있다. 아래의 예제는 status code가 200일 때는 { success:boolean }의 response type을 적용하고 status code가 400번대일 때는 { error:boolean } response type을 적용한다.

type MemberRouteGet = {
  Querystring: { username: string };
  Headers: { authorization: string };
  Reply: {
    200: { success: boolean };
    "4xx": { error: boolean };
  };
};

fastify.get<MemberRouteGet>("/", (request, reply) => {
  console.log(" ::: member get ::: ");
  const { username } = request.query;
  const { authorization } = request.headers;
  if (authorization) {
    reply.code(200).send({ success: true });
    return;
  }
  reply.code(400).send({ error: true });
});

위의 예제를 기반으로 구성한 간단한 member route의 구성은 다음과 같다.

import { FastifyInstance, FastifyPluginOptions } from "fastify";

type MemberRouteResponse = {
  Reply: {
    200: { success: boolean };
    "4xx": { error: boolean };
  };
};

type MemberRouteHeader = {
  Headers: { authorization: string };
};

type MemberRouteBody = {
  Body: { address: string; name: string };
};

type MemberRouteParam = {
  Params: { memberid: string };
};

type MemberRouteGet = {
  Querystring: { username: string };
} & MemberRouteHeader &
  MemberRouteResponse;

type MemberRoutePost = MemberRouteHeader &
  MemberRouteBody &
  MemberRouteResponse;

type MemberRoutePatch = MemberRouteHeader &
  MemberRouteParam &
  MemberRouteBody &
  MemberRouteResponse;

type MEmberRouteDelete = MemberRouteHeader &
  MemberRouteParam &
  MemberRouteResponse;

const memberRoutes = (
  fastify: FastifyInstance,
  options: FastifyPluginOptions
) => {
  fastify.get<MemberRouteGet>("/", (request, reply) => {
    const { username } = request.query;
    const { authorization } = request.headers;
    if (authorization) {
      reply.code(200).send({ success: true });
      return;
    }
    reply.code(400).send({ error: true });
  });

  fastify.post<MemberRoutePost>("/", (request, reply) => {
    const { address, name } = request.body;
    const { authorization } = request.headers;
    if (authorization) {
      reply.code(200).send({ success: true });
      return;
    }
    reply.code(400).send({ error: true });
  });

  fastify.patch<MemberRoutePatch>("/", (request, reply) => {
    const { memberid } = request.params;
    const { address, name } = request.body;
    const { authorization } = request.headers;
    if (authorization) {
      reply.code(200).send({ success: true });
      return;
    }
    reply.code(400).send({ error: true });
  });

  fastify.delete<MEmberRouteDelete>("/:memberId", (request, reply) => {
    const { memberid } = request.params;
    const { authorization } = request.headers;
    if (authorization) {
      reply.code(200).send({ success: true });
      return;
    }
    reply.code(400).send({ error: true });
  });
};

export default memberRoutes;

이제 해당 router를 fastify에 등록해 사용해보자. 아래 예제처럼 route를 등록하고 npm run dev command를 통해 server를 실행하면 member route가 정상적으로 동작하는 것을 확인할 수 있다.

import Fastify from "fastify";
import memberRoutes from "./routes/member.js";

const fastify = Fastify({
  logger: true,
});

fastify.register(memberRoutes, { prefix: "/member" });

const serverStart = async () => {
  try {
    await fastify.listen({ port: 3000 });
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
serverStart();

Type Provider

Type provider를 사용하면 route option에 추가되는 schema 객체를 기반으로 type을 추론해 사용하거나 schema json 파일을 기반으로 type을 생성할 수도 있다. Fastify에서 지원하는 type provider중 선택지는 몇 가지 있지만 해당 포스트에선 typebox type provider를 통해 schema 객체를 기반으로 type을 추론하여 적용하는 예제를 살펴본다. ( Reference - Type-Providers )

먼저 typebox type provider를 적용하기 위해선 다음 package가 필요하다.

npm i -D @fastify/type-provider-typebox @sinclair/typebox

예제를 간단히 하기 위해 post route만 별도로 분리하여 schema 객체를 적용하고 schema를 통해 추론한 type이 적용되는 살펴보자.

app.ts

...
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";

const fastify = Fastify({
  logger: true,
}).withTypeProvider<TypeBoxTypeProvider>();

export type MyFastifyInstance = typeof fastify;

...

member.ts

import { Type } from "@sinclair/typebox";
import { MyFastifyInstance } from "../app.js";

const memberRoutePostSchema = {
  body: Type.Object({
    address: Type.String(),
    name: Type.Optional(Type.String()),
  }),
  headers: Type.Object({
    authorization: Type.String(),
  }),
};

const memberRoutes = (
  fastify: MyFastifyInstance,
  options: FastifyPluginOptions
) => {
  fastify.post(
    "/",
    {
      schema: memberRoutePostSchema,
    },
    (request, reply) => {
      const { address, name } = request.body;
      const { authorization } = request.headers;
      if (authorization) {
        reply.code(200).send({ success: true });
        return;
      }
      reply.code(400).send({ error: true });
    }
  );

  ...

};

위의 코드에서 확인할 수 있듯이 post route에 별도의 타입을 generic type으로 넘겨주지 않아도 router schema option에 적용된 memberRoutePostSchema를 기반으로 route hnadler에서 request body와 header의 타입을 추론해준다.

More from this blog

[ 살펴보기 ] TypeORM - Transactions, Migration

Transation Database 종류에 따라 detail한 부분은 차이점이 조금씩 있겠지만 각 sql statement는 개별적인 transaction block을 통해 실행되며 Database 설정에 따라 sql statement의 실행 결과가 자동으로 commit되어 영구히 적용되거나 commit을 직접 실행하기 전까지는 영구히 적용되지 않을 수 있다. 대부분의 경우 default로 sql statement 실행 결과가 자동으로 comm...

Feb 9, 20256 min read
[ 살펴보기 ] TypeORM - Transactions, Migration

Dev Diary

184 posts