Skip to content
Public template

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

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