Skip to content

Repository files navigation

My Service Template

Шаблон HTTP-сервиса на Node.js 22, TypeScript и Fastify 5. Подходит для старта API или микросервиса: конфигурация проверяется до открытия порта, маршруты валидируют входные данные, ошибки имеют общий JSON-контракт, а приложение можно тестировать без сетевого соединения.

База данных, брокер, аутентификация и бизнес-логика выбираются под конкретный сервис. В репозитории есть небольшой модуль greeting, показывающий связь маршрута и сервиса.

Содержание

Быстрый старт

Требуются Node.js 22.13+ и npm 10+. Файл .nvmrc задаёт Node.js 22 для локальной разработки и CI. Docker нужен только для контейнерного запуска.

# При использовании nvm:
nvm install
nvm use

npm ci
cp .env.example .env
npm run dev

npm run dev запускает сервер через tsx watch с перезапуском при изменениях исходников. Команды dev и start загружают .env из текущей директории штатным механизмом Node.js. Файл необязателен; уже заданные переменные процесса имеют приоритет. После изменения .env перезапустите команду.

Проверьте сервис из другого терминала:

curl -i http://localhost:3000/health
curl -i http://localhost:3000/ready
curl http://localhost:3000/api/v1/hello/World
# {"message":"Hello, World!"}

Документация доступна на localhost:3000/docs/, метрики — на localhost:3000/metrics. CORS в .env.example отключён; для фронтенда на другом origin задайте, например, CORS_ORIGINS=http://localhost:5173.

Команды разработки

Команда Назначение
npm ci Установить точные версии из package-lock.json
npm run dev Запустить TypeScript-сервер с наблюдением за файлами
npm run build Скомпилировать src/ в dist/, включая source maps и типы
npm start Запустить ранее собранный dist/server.js
npm run typecheck Проверить типы исходников и тестов без генерации файлов
npm run lint Проверить ESLint; предупреждения считаются ошибкой
npm run format Отформатировать проект
npm run format:check Проверить форматирование без изменения файлов
npm test Однократно запустить Vitest
npm run test:watch Перезапускать тесты при изменениях
npm run test:coverage Запустить тесты и проверить пороги покрытия
npm run audit:prod Проверить production-зависимости; порог отказа — high
npm run check Форматирование → ESLint → типы → тесты с покрытием → сборка

Перед pull request выполните npm run check. Аудит зависимостей запускается отдельно, поскольку требует доступа к npm registry; CI выполняет обе команды.

Структура и зависимости

src/
├── app.ts                         # сборка приложения и передача зависимостей
├── server.ts                      # запуск порта и обработка ошибок старта
├── config/
│   └── env.ts                     # Zod-схема и тип AppConfig
├── lib/
│   ├── errors.ts                  # контролируемая ошибка AppError
│   ├── error-response.ts          # JSON-контракт ошибки и его схема
│   ├── readiness.ts               # параллельные проверки с таймаутом
│   └── shutdown.ts                # сигналы и ограничение времени остановки
├── modules/greeting/
│   ├── greeting.routes.ts         # HTTP-схемы и вызов сервиса
│   └── greeting.service.ts        # пример прикладной логики
├── plugins/
│   ├── documentation.ts           # OpenAPI и Swagger UI
│   ├── error-handler.ts           # перевод исключений в HTTP-ответы
│   ├── metrics.ts                 # отдельный Prometheus registry на приложение
│   ├── request-context.ts         # request ID и настройки логирования
│   └── security.ts                # Helmet, CORS и rate limit
└── routes/
    └── health.ts                  # /health и /ready

test/
├── helpers/                       # тестовая конфигурация и закрытие приложений
└── *.test.ts                      # HTTP-контракты, конфигурация и lifecycle

buildApp() создаёт приложение без открытия порта и без подписки на сигналы процесса. Фабрика принимает config, readinessChecks и greetingService. Если конфигурация не передана, используется loadConfig(). Для изоляции тестов передавайте её явно.

Направление зависимостей: маршрут вызывает прикладной сервис, а app.ts создаёт и передаёт его зависимости. Прикладной сервис не импортирует Fastify и не читает process.env. Функции register* подключают общие HTTP-hooks в корневой области Fastify; модули и операционные маршруты регистрируются как отдельные плагины через app.register().

Конфигурация

Источник правил и значений по умолчанию — src/config/env.ts. .env.example содержит пример локальных настроек. loadConfig(environment) проверяет переданный объект, не изменяя окружение и не читая файлы самостоятельно. Неизвестные переменные игнорируются. Ошибка конфигурации перечисляет неверные поля и останавливает запуск до открытия порта.

Переменная По умолчанию Правила и назначение
NODE_ENV development development, test, production
HOST 0.0.0.0 Непустой адрес для прослушивания
PORT 3000 Целое число от 1 до 65535
LOG_LEVEL info fatal, error, warn, info, debug, trace, silent
SERVICE_NAME my-service Имя в health, логах и OpenAPI
SERVICE_VERSION 0.0.0 Версия в health, логах и OpenAPI
CORS_ORIGINS пустая строка Отключён, * или HTTP(S) origins через запятую
TRUST_PROXY false Доверие к заголовкам прокси
DOCS_ENABLED true вне production Публикация /docs/, /docs/json, /docs/yaml
METRICS_ENABLED true Публикация /metrics и сбор метрик
RATE_LIMIT_MAX 100 Положительное безопасное целое; запросы на IP за минуту
READINESS_TIMEOUT_MS 2000 Таймаут каждой проверки зависимости
SHUTDOWN_TIMEOUT_MS 10000 Максимальное время закрытия приложения

Булевы значения задаются только строками true и false; 1, yes и пустая строка недопустимы. Таймауты — целые числа от 1 до 2147483647 миллисекунд. Значение SERVICE_VERSION не берётся из package.json: задавайте его при выпуске; в примере .env указано 1.0.0.

В CORS_ORIGINS пробелы вокруг элементов и дубликаты удаляются. Используйте origin в каноническом виде, например https://app.example.com или http://localhost:5173, без пути, завершающего /, query и учётных данных. * задаётся отдельно от списка.

Пример запуска собранного приложения:

npm run build
NODE_ENV=production SERVICE_NAME=orders SERVICE_VERSION=1.2.3 npm start

HTTP API и ошибки

Метод Путь Результат
GET /api/v1/hello/:name 200, { "message": "Hello, World!" }
GET /health 200, состояние процесса
GET /ready 200 при готовности, иначе 503
GET /metrics Метрики Prometheus, если включены
GET /docs/ Swagger UI, если включён
GET /docs/json OpenAPI JSON, если документация включена
GET /docs/yaml OpenAPI YAML, если документация включена

Имя в greeting содержит от 1 до 80 символов; специальные символы в URL кодируются. Fastify использует JSON Schema для валидации параметров и сериализации ответа. Те же схемы попадают в OpenAPI. При отключённых docs и metrics соответствующие пути возвращают 404.

Ошибки маршрутов, валидации и HTTP-парсера используют общий формат:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "params/name must NOT have more than 80 characters",
    "requestId": "example-request-id"
  }
}
Ситуация HTTP error.code
Параметры не соответствуют схеме 400 VALIDATION_ERROR
Неизвестный маршрут 404 NOT_FOUND
Невалидный JSON 400 FST_ERR_CTP_INVALID_JSON_BODY
Тело превышает лимит Fastify 413 FST_ERR_CTP_BODY_TOO_LARGE
Неподдерживаемый Content-Type 415 FST_ERR_CTP_INVALID_MEDIA_TYPE
Превышен rate limit 429 RATE_LIMIT_EXCEEDED
Необработанное исключение 500 INTERNAL_ERROR

В шаблоне действует стандартный лимит тела Fastify — 1 MiB. Другие HTTP-ошибки 4xx сохраняют статус и код Fastify; если кода нет, используется HTTP_ERROR.

Для ожидаемого отказа используйте AppError:

throw new AppError('Order already exists', 409, 'ORDER_EXISTS', { orderId });

statusCode должен быть целым от 400 до 599. message и необязательные details считаются публичными и передаются клиенту, в том числе для AppError со статусом 5xx. Не помещайте туда секреты. Неизвестные серверные ошибки записываются в лог; клиент получает только Internal server error. Ответ /ready со статусом 503 имеет собственную схему с состояниями зависимостей, описанную ниже.

Health и readiness

/health возвращает status, service, version, timestamp и uptimeSeconds процесса. Он не обращается к внешним системам: временная недоступность БД не означает, что процесс нужно перезапустить.

/ready проверяет, способен ли экземпляр обслуживать запросы. Без зависимостей он возвращает 200 и пустой checks. Подключайте реальные проверки в месте сборки приложения:

const app = await buildApp({
  config,
  readinessChecks: {
    database: async () => {
      await database.ping();
    },
    catalog: async (signal) => {
      const response = await fetch('http://catalog:3000/health', { signal });
      if (!response.ok) throw new Error('Catalog unavailable');
    },
  },
});

app.addHook('onClose', async () => {
  await database.close();
});

database в примере — ваш предварительно созданный клиент. Проверка возвращает void или Promise<void>; успешное завершение означает up, исключение или таймаут — down. Возвращать false для обозначения ошибки не нужно: проверка должна выбросить исключение.

Все проверки запускаются параллельно при каждом запросе. Каждая ограничена READINESS_TIMEOUT_MS; синхронное исключение не мешает остальным проверкам. При таймауте переданный AbortSignal отменяется. HTTP-ответ ограничен по времени и для клиентов без поддержки отмены, но их фоновая операция может продолжиться: задайте также таймаут драйвера. Проверки не должны блокировать event loop синхронной работой.

Пример ответа 503:

{
  "status": "not_ready",
  "service": "my-service",
  "version": "1.0.0",
  "timestamp": "2026-09-10T12:00:00.000Z",
  "checks": { "database": "down", "catalog": "up" }
}

Тексты исключений не раскрываются через readiness. В оркестраторе используйте /health для liveness и /ready для readiness; HTTP-таймаут readiness probe должен быть больше READINESS_TIMEOUT_MS с запасом на сеть.

Логи и метрики

Логи — JSON в stdout, с полями service, version и идентификатором запроса reqId. Fastify пишет начало и завершение запросов; обработчик ошибок добавляет серверные ошибки. В маршрутах используйте request.log, вне запроса — app.log.

Входящий x-request-id принимается, если содержит от 1 до 128 символов из набора A–Z, a–z, 0–9, ., _, :, -. В остальных случаях создаётся UUID. Идентификатор возвращается заголовком x-request-id и в JSON ошибки. Это средство корреляции, не подтверждение личности клиента.

Настроено редактирование req.headers.authorization, req.headers.cookie и res.headers["set-cookie"]. Произвольные поля ваших сообщений и секреты в URL автоматически не очищаются; не записывайте в логи токены и полные тела запросов.

При METRICS_ENABLED=true создаётся отдельный registry на экземпляр приложения:

Метрика Содержание
service_http_requests_total Счётчик завершённых запросов
service_http_request_duration_seconds Гистограмма времени обработки в секундах
service_process_*, service_nodejs_* Стандартные метрики процесса и Node.js

Метки HTTP-метрик: method, route, status_code. route содержит шаблон /api/v1/hello/:name, для неизвестного маршрута — unknown; имена пользователей и request ID в метки не попадают. Учитываются ошибки, 429 и операционные запросы. Текущий запрос /metrics будет учтён после завершения ответа, поэтому виден в следующем scrape.

Пример конфигурации Prometheus в одной Docker-сети с сервисом:

scrape_configs:
  - job_name: my-service
    metrics_path: /metrics
    static_configs:
      - targets: ['service:3000']

Безопасность HTTP

Helmet добавляет защитные заголовки. CORS отключён по умолчанию; явный allowlist разрешает браузерный доступ только перечисленным origins. CORS не заменяет аутентификацию и не запрещает прямые HTTP-запросы от других клиентов.

Rate limit действует по IP с минутным окном; превышение возвращает 429 и retry-after. /health, /ready и /metrics исключены из ограничения. Состояние лимитера хранится в памяти процесса и не разделяется между репликами; при масштабировании подключите общее хранилище или ограничение на ingress. Для неизвестных маршрутов отдельный лимитер здесь не подключён.

TRUST_PROXY=true включает доверие к заголовкам прокси, влияющим в том числе на IP клиента. Используйте этот режим за контролируемым ingress с закрытым прямым доступом к сервису. Для выборочного доверия конкретным адресам расширьте конфигурацию trustProxy в app.ts.

В production документация по умолчанию отключена. Доступ к метрикам и внутренним пробам ограничивайте на уровне сети или ingress. TLS и аутентификация в шаблон не включены.

Добавление модуля

  1. Создайте src/modules/orders/orders.service.ts с прикладной логикой и явными зависимостями.
  2. Создайте orders.routes.ts: типы параметров, JSON Schema входа и ответа, вызов сервиса.
  3. Создайте зависимости в app.ts и зарегистрируйте маршруты с префиксом /api/v1.
  4. Добавьте HTTP-тесты через app.inject() и проверки важных ветвей прикладной логики.
  5. Подключите readiness и onClose для внешних клиентов, обновите конфигурацию и документацию.

Пример регистрации, когда orderRepository и классы модуля уже реализованы:

const orders = new OrdersService(orderRepository);
await app.register(ordersRoutes, { prefix: '/api/v1', service: orders });

Маршруты получают service через options, как greetingRoutes. Это позволяет заменять зависимости в тестах без моков импортов. Для схем ошибок используйте errorResponseSchema из src/lib/error-response.ts, например в response: { '4xx': errorResponseSchema }. Схемы маршрутов, добавленных после подключения Swagger, включаются в OpenAPI автоматически.

Сохраняйте ESM-импорты с расширением .js даже внутри .ts: после компиляции Node.js запускает эти пути непосредственно. tsconfig.json включает строгий режим, проверку неопределённых индексов и точную семантику optional-полей.

Тестирование

HTTP-тестам не нужны открытые порты, Docker или внешние сервисы:

import { expect, it } from 'vitest';
import { createTestApp } from './helpers/app.js';

it('returns a greeting', async () => {
  const app = await createTestApp();
  const response = await app.inject('/api/v1/hello/World');

  expect(response.statusCode).toBe(200);
  expect(response.json()).toEqual({ message: 'Hello, World!' });
});

createTestApp() передаёт тестовую конфигурацию и регистрирует закрытие приложения после теста, включая случай падения assertions. Для собственных экземпляров также вызывайте app.close(). Тесты охватывают конфигурацию, контракты HTTP-ошибок, CORS и proxy headers, rate limit, readiness с отменой, изоляцию метрик и обработку сигналов остановки.

Пороги покрытия: 80% строк, функций и statements, 75% ветвей. Отчёты находятся в coverage/, включая lcov.info. Из покрытия исключён только исполняемый src/server.ts; логика завершения процесса в src/lib/shutdown.ts тестируется отдельно. При изменении bootstrap дополнительно проверьте реальный запуск, занятый порт и остановку процесса.

Запуск и остановка

Для запуска вне watch-режима выполните npm run build, затем npm start. Source maps включены для стектрейсов собранного приложения. Изменения в src/ требуют повторной сборки.

При SIGINT или SIGTERM сервер вызывает app.close(): закрывает HTTP-сервер, дожидается текущих запросов и выполняет onClose hooks. Повторные сигналы во время этой процедуры не запускают её заново. После успешного завершения Node.js выходит естественным образом, когда не остаётся активных ресурсов.

Ошибка закрытия или превышение SHUTDOWN_TIMEOUT_MS приводит к выходу с кодом 1. Ваши подключения, интервалы и фоновые задачи должны освобождаться в onClose. При ошибке открытия порта приложение также закрывается, процесс завершается с кодом 1.

Docker

docker compose up --build -d
docker compose logs -f service
curl http://localhost:3000/health
docker compose down

Dockerfile использует multi-stage build: зависимости устанавливаются по lockfile, TypeScript компилируется, dev-зависимости удаляются. Runtime содержит dist/, production-зависимости и запускается непривилегированным пользователем. Исходники и .env в образ не копируются.

Compose задаёт NODE_ENV=production, внутренний порт 3000 и отключает docs. Файловая система контейнера доступна только для чтения; /tmp находится в tmpfs. Включены init, no-new-privileges и ожидание остановки 15s, что превышает стандартный shutdown-таймаут 10s. При увеличении SHUTDOWN_TIMEOUT_MS увеличьте и stop_grace_period.

.env для локального Node.js-запуска не передаётся в контейнер автоматически. Задайте нужные значения в environment файла compose.yaml либо подключите отдельный env_file. Переменные из environment имеют приоритет над env_file.

Запуск образа без Compose, с другим внутренним и внешним портом:

docker build -t my-service:local .
docker run --rm --init -p 8080:8080 \
  -e PORT=8080 -e SERVICE_NAME=my-service -e SERVICE_VERSION=1.0.0 \
  my-service:local

Встроенный Docker healthcheck обращается к /health на ${PORT:-3000}. Для контейнера сохраняйте HOST=0.0.0.0; привязка к loopback сделает опубликованный порт недоступным. Для readiness оркестратора настройте отдельную пробу /ready.

CI и создание нового сервиса

GitHub Actions выполняет две задачи при pull request и push в main:

  • quality: установка через npm ci, npm run check, аудит production-зависимостей, загрузка отчёта покрытия;
  • container: сборка Docker-образа с BuildKit cache без публикации.

Dependabot проверяет npm-пакеты и GitHub Actions еженедельно. Формы issues и шаблон PR находятся в .github/.

При использовании репозитория как шаблона:

  1. Включите Settings → General → Template repository, затем используйте Use this template.
  2. Замените имя и описание пакета в package.json; синхронизируйте lockfile командой npm install --package-lock-only.
  3. Обновите SERVICE_NAME, SERVICE_VERSION, название README, теги образа в CI и владельца лицензии. Проверьте defaults в src/config/env.ts и значения в .env.example.
  4. Замените greeting бизнес-модулями и их тестами; выберите хранилища и механизм авторизации.
  5. Настройте branch protection для main с обязательными проверками quality и container.
  6. Включите private vulnerability reporting, добавьте командный CODEOWNERS и настройте публикацию образов и деплой под свою инфраструктуру.

Правила участия: CONTRIBUTING.md. Сообщение об уязвимостях: SECURITY.md. Лицензия: MIT.

Диагностика

Симптом Что проверить
Invalid environment configuration Названные поля, формат boolean, диапазон порта и таймаутов
Значения .env не применились Рабочую директорию, переменные shell с приоритетом, перезапуск команды
EADDRINUSE Занятый порт; завершите другой процесс или задайте PORT
npm start не находит dist/server.js Выполните npm run build
/docs/ возвращает 404 NODE_ENV и DOCS_ENABLED; в production это поведение по умолчанию
/ready возвращает 503 Поле checks, доступность зависимостей и их таймауты
Браузер блокирует запрос, а curl работает Точный origin в CORS_ORIGINS, включая схему и порт
Ответ 429 RATE_LIMIT_MAX, retry-after и корректность TRUST_PROXY
Контейнер недоступен или unhealthy Логи, HOST, PORT и соответствие опубликованного порта внутреннему
Процесс выходит с кодом 1 при остановке Ошибки onClose, незавершённые запросы и shutdown-таймаут

About

An opinionated Node.js and TypeScript service starter where observability, security, testing, and delivery are defaults—not afterthoughts.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages