Вступление
Зачастую возникает необходимость начать новый микросервис. Вот и у меня совсем недавно возникла такая потребность. А ведь хочется еще и чего-то новенького попробовать.
Сперва был определен стек и хотя процесс для меня не новый, но я столкнулся с множеством подводных камней. В результате родилась идея написать этот туториал.
В конце будет представлена ссылка на репозиторий с кодом.
Шаг 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/
Добавить комментарий