Вступление
Зачастую возникает необходимость начать новый микросервис. Вот и у меня совсем недавно возникла такая потребность. А ведь хочется еще и чего-то новенького попробовать.
Сперва был определен стек и хотя процесс для меня не новый, но я столкнулся с множеством подводных камней. В результате родилась идея написать этот туториал.
В конце будет представлена ссылка на репозиторий с кодом.
Шаг 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
Комментарии (4)
 - AlexGorky00.00.0000 00:00- Fastify достаточно легкий и на нем быстро и просто писать REST. У него удобная система плагинов, собственно почему бы и нет. - Вообще-то забыли написать, что fastify - один из самых быстрых (если не самый быстрый) фреймворк для создания веб-сервисов для node.js. Об этом, например написано тут: https://habr.com/ru/post/555668/  - DmitryVoronkov00.00.0000 00:00- Сперва был определен стек и хотя процесс для меня не новый, но я столкнулся с множеством подводных камней. В результате родилась идея написать этот туториал. - Хотелось бы в статье указать где именно были "подводные камни" и какие были варианты решения. 
 
 - Northerner1900.00.0000 00:00- Спасибо за статью! 
 В prisma можно использовать маппинг чтобы работать с существующими коллекциями- model User {
 // fields
 @@map('users')
 }
 
           
 
rutexd
Для тестов можно подключить TestyTs. Простой и минимальный.