Стартуем микросервис на Node.js + fastify + Typescript + prisma + mongodb + grpc

от автора

Вступление

Зачастую возникает необходимость начать новый микросервис. Вот и у меня совсем недавно возникла такая потребность. А ведь хочется еще и чего-то новенького попробовать.

Сперва был определен стек и хотя процесс для меня не новый, но я столкнулся с множеством подводных камней. В результате родилась идея написать этот туториал.

В конце будет представлена ссылка на репозиторий с кодом.

Шаг 1. Инициализация проекта на TS

Для начала необходимо инициализировать сам typescript и произвести первоначальные настройки.

Запуск проекта на тайпскрипте с нуля — задача достаточно тривиальна, но почему-то постоянно возникают сложности с настройкой. В этом плане очень удобен nest, так как там идет все из коробки, но он достаточно тяжелый и сам по себе уже как язык программирования, поэтому это не наш путь.

Выполним следующие команды в терминале

npm init -y yarn add typescript tsconfig-paths ts-node @types/node nodemon concurrently --dev mkdir src && touch src/index.ts npx tsc --init --rootDir src --outDir build --esModuleInterop --resolveJsonModule --module commonjs --allowJs true --noImplicitAny true --target esnext --moduleResolution node touch .gitignore && echo "node_modules \n.vscode\nbuild" >> .gitignore touch nodemon.json echo "console.log('Hello world')" >> src/index.ts

Далее дополним конфигурационный файл typescript.json, первоначальную настройку трогать не будем, ее вполне достаточно для наших целей

{   "compilerOptions": {...},   "include": ["src"],   "exclude": [     "node_modules",     "dist",     "examples",   ], } 

Заполним секцию scripts в package.json

  "scripts": {     "test": "echo \"Error: no test specified\" && exit 1",     "build": "tsc",     "dev": "export NODE_ENV=development TS_NODE_BASEURL=./dist && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"tsc -w\" \"nodemon\""   },

Для dev режима будет использоваться nodemon, поэтому его тоже необходимо настроить, в nodemon.json

{   "ignore": [     "**/*.test.ts",     "**/*.spec.ts",     ".git",     "node_modules"   ],   "watch": [     "src"   ],   "exec": "tsc && node -r tsconfig-paths/register -r ts-node/register build/index.js",   "ext": "ts, js" }

Финальная проверка на этом шаге, в консоли после запуска мы должны увидеть сообщение

yarn dev  [App] [nodemon] starting `tsc && node -r tsconfig-paths/register -r ts-node/register build/index.js` [App] Hello world [App] [nodemon] clean exit - waiting for changes before rest

На этом первоначальная, базовая, настройка готова.

Шаг 2. Добавление Fastify

Далее нам необходимо добавить сам сервер. Сперва отвечу на вопрос: «Почему fastify ? «

Fastify достаточно легкий и на нем быстро и просто писать REST. У него удобная система плагинов, собственно почему бы и нет.

Добавим пакет и создадим файл для нашего сервера.

yarn add fastify touch src/app.ts

На данном этапе наш сервер достаточно прост, создание самого приложения вынесем в отдельный файл app.ts.

import Fastify, { FastifyServerOptions } from 'fastify'  export type AppOptions = Partial<FastifyServerOptions>;  async function buildApp(options: AppOptions = {}) {   const fastify = Fastify(options);    return fastify; }  export { buildApp }

Опишем запуск сервера в index.ts

import { buildApp, AppOptions } from './app';  const options: AppOptions = {   logger: true, };  const start = async () => {   const app = await buildApp(options);    try {     await app.listen({       port: 3000,       host: 'localhost',     });    } catch (err) {     app.log.error(err);     process.exit(1);   } };  start();

Для проверки запускаем сервер в dev режиме, в консоли должно появиться следующее сообщение.

yarn dev  [App] {"level":30,"time":1676434938643,"pid":39600,"hostname":"Anatolys-MacBook-Pro.local","msg":"Server listening at http://[::1]:3000"}

Шаг 3. Добавление prisma

В качестве базы данных будет использоваться Mongodb. И собственно почему Prisma ?
Впервые, не так давно, попробовал prisma и был в восторге — описываешь схему, а она сама уже генерит тонны логики, что сильно упрощает взаимодействие с БД.

Вам нужно поднять локально монгу или взять в докер хабе https://hub.docker.com/_/mongo.

Предположим что у вас уже есть монга по адресу mongodb://localhost:27017

Сперва необходимо установить необходимые пакеты

yarn add prisma @prisma/client fastify-plugin npx prisma init --datasource-provider mongodb

Далее определить схему базы данных prisma/schema.prisma

generator client {   provider = "prisma-client-js" }  datasource db {   provider = "mongodb"   url      = "mongodb://localhost:27017/example" }  model Post {   id       String    @id @default(auto()) @map("_id") @db.ObjectId   slug     String    @unique   title    String   body     String   author   User      @relation(fields: [authorId], references: [id])   authorId String    @db.ObjectId }  model User {   id      String   @id @default(auto()) @map("_id") @db.ObjectId   email   String   @unique   name    String?   address Address?   posts   Post[] }

Запускаем генерацию бизнес логики работы с БД и накатываем изменения в саму базу данных. Это действие необходимо делать каждый раз после изменения схемы, поэтому их можно вынести в секцию scrips — package.json

yarn prisma generate npx prisma db push

Подключать призму мы будем через плагины fastify touch src/prisma.plugin.ts

import { PrismaClient } from '@prisma/client'; import fp from 'fastify-plugin';  async function initDatabaseConnection(): Promise<PrismaClient> {   const db = new PrismaClient();   await db.$connect();   return db; }  // Use TypeScript module augmentation to declare the type of server.prisma to be PrismaClient declare module 'fastify' {   interface FastifyInstance {     prisma: PrismaClient   } }  const prismaPlugin = fp(async (server) => {   const prisma = await initDatabaseConnection();    // Make Prisma Client available through the fastify server instance: server.prisma   server.decorate('prisma', prisma);   server.addHook('onClose', async () =>  {     await server.prisma.$disconnect();   }); });  export default prismaPlugin; 

Далее необходимо подключить плагин в app.ts

import Fastify, { FastifyServerOptions } from 'fastify' import prismaPlugin from './prisma.plugin';  export type AppOptions = Partial<FastifyServerOptions>;  async function buildApp(options: AppOptions = {}) {   const fastify = Fastify(options);   fastify.register(prismaPlugin);      return fastify; }  export { buildApp }

Сделаем простенький обработчик для проверки работoспособности touch src/services.ts

import { PrismaClient } from '@prisma/client';  async function main(prisma: PrismaClient) {   await prisma.user.deleteMany();   await prisma.user.create({ data: { email: 'test@email.com' } });   const usersCount = await prisma.user.count();   console.log({ users_count: usersCount });  }  export { main };

И наконец, внесем изменения в index.js

import { buildApp, AppOptions } from './app'; import { main } from './services';  const options: AppOptions = {   logger: true, };  const start = async () => {   const app = await buildApp(options);    try {     await app.listen({       port: 3000,       host: 'localhost',     });      await main(app.prisma);   } catch (err) {     app.log.error(err);     process.exit(1);   } };  start();

Финальная проверка на шаге 3, в консоли должно появиться следующее сообщение

yarn dev  [App] { users_count: 1 }

Шаг 4. Добавление grpc

И вот мы подошли к финишу, осталось добавить сервер для grpc, а собственно для чего нам grpc?

Grpc хорош для взаимодействий бек-бек, он шустрее реста, но имплементируется сложнее.

Сперва необходимо установить необходимые пакеты.

yarn add @grpc/grpc-js @grpc/proto-loader mkdir proto touch proto/example.proto

Затем опишем наш сервер со стороны proto файла example.proto, для примера достаточно описать один сервис, который будет отдавать массив пользователей:

syntax = "proto3";  package example;  message User {   string id = 1;   string email = 2; };  message GetUsersResponse {   repeated User users = 1; }  message GetUsersRequest {}  service UserService {   rpc GetUsers(GetUsersRequest) returns (GetUsersResponse); }

После того как мы описали proto файл, необходимо сгенерить типы для typescript, для этого воспользуемся встроенным в proto-loader генератором proto-loader-gen-types. Сразу добавим нужную команду в секцию scrips в файле package.json

  "scripts": {     "test": "echo \"Error: no test specified\" && exit 1",     "build": "tsc",     "dev": "export NODE_ENV=development TS_NODE_BASEURL=./dist && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"tsc -w\" \"nodemon\"",     "gen-proto": "$(npm bin)/proto-loader-gen-types --longs=String --enums=String  --oneofs --grpcLib=@grpc/grpc-js --outDir=src/proto/interfaces proto/*.proto"   }, 

После запускаем генерацию типов yarn gen-proto

Все сгенерированные типы будут лежать в src/proto/interfaces

Подключать grpc server будем так же через fasify плагины. Для этого создадим файл touch src/grpc.plugin.ts и запишем в него следующий код реализации сервера.

import { GetUsersResponse } from './proto/interfaces/example/GetUsersResponse'; import { GetUsersRequest__Output } from './proto/interfaces/example/GetUsersRequest'; import fp from 'fastify-plugin'; import { join } from 'path'; import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; import { ProtoGrpcType } from './proto/interfaces/example';  declare module 'fastify' {   interface FastifyInstance {     grpcServer: {       start: () => void,     },   } }  const grpcServerOptions = {   keepCase: false,   longs: String,   enums: String,   defaults: false,   oneofs: true, };  const grpcServerPlugin = fp(async (fastify) => {   // load proto files from directory   const packageDefinition = protoLoader.loadSync([join(__dirname, '../proto/example.proto')], grpcServerOptions);   const proto = grpc.loadPackageDefinition(packageDefinition) as unknown as ProtoGrpcType;   const grpcServer = new grpc.Server();    // mapping between handlers and rpc services   grpcServer.addService(proto.example.UserService.service, {     GetUsers: async (       req: grpc.ServerUnaryCall<GetUsersRequest__Output, GetUsersResponse>,       res: grpc.sendUnaryData<GetUsersResponse>) => {         return res(null, {           users: [{             id: 'test',             email: 'test',           }],         })     },   });    function start(opts: { port: number } = { port: 50501 }) {     return grpcServer.bindAsync(       `0.0.0.0:${opts.port}`,       grpc.ServerCredentials.createInsecure(),       (err, port) => {         if (err) {           console.error(err);           return;         }          grpcServer.start();         console.log(`GRPC Server listening on ${port}`);       },     );   }    fastify.decorate('grpcServer', { start }); });  export { grpcServerPlugin };

Ответ обработчика ендпоинта GetUsers здесь замокан в демонстрационных целях.

После того как плагин готов — необходимо его подключить в app.ts

import Fastify, { FastifyServerOptions } from 'fastify' import { grpcServerPlugin } from './grpc.plugin'; import prismaPlugin from './prisma.plugin';  export type AppOptions = Partial<FastifyServerOptions>;  async function buildApp(options: AppOptions = {}) {   const fastify = Fastify(options);   fastify.register(prismaPlugin);   fastify.register(grpcServerPlugin);    return fastify; }  export { buildApp }

Теперь плагин подключен и осталось только запустить сервер в index.ts

import { buildApp, AppOptions } from './app'; import { main } from './services';  const options: AppOptions = {   logger: true, };  const start = async () => {   const app = await buildApp(options);    try {     await app.listen({       port: 3000,       host: 'localhost',     });      app.grpcServer.start();      await main(app.prisma);   } catch (err) {     app.log.error(err);     process.exit(1);   } };  start();

Для проверки запустим наш серверyarn dev, в консоли должна оторбазиться следующая информация

yarn dev  [App] {"level":30,"time":1676521988065,"pid":69311,"hostname":"Anatolys-MacBook-Pro.local","msg":"Server listening at http://127.0.0.1:3000"} [App] GRPC Server listening on 50501 [App] { users_count: 1 }

Проверить ответ можно либо при помощи теста, либо через стороннее приложение, я использовал bloomRPC.

В ответе должен прийти наш замоканый ответ.

Заключение

Подведем итоги, болванка микросервиса может принимать запросы по grpc и по rest одновременно. Rest запущен на 3000 порту, grpc на 50501.

В качестве ORM используется prisma. Пожалуй не хватает только тестов, но это уже за рамками данного туториала!

Ссылка на репо с проектом: https://github.com/iseekyouu/habr-tfpg


ссылка на оригинал статьи https://habr.com/ru/post/718774/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *