refactor(services): integrate @bytelyst/fastify-core into all 4 services

Replaced duplicated server setup code with createServiceApp() factory:
- platform-service: 91 → 39 lines
- billing-service: 105 → 51 lines (keeps service-specific internal key auth)
- growth-service: 83 → 33 lines
- tracker-service: 88 → 36 lines

Enhanced fastify-core with optional Swagger + Prometheus metrics support.
Total reduction: ~208 lines of duplicated boilerplate eliminated.
All 246 tests pass.
This commit is contained in:
saravanakumardb1 2026-02-12 22:53:22 -08:00
parent a4099c8f9b
commit 63c08dbb0a
12 changed files with 119 additions and 260 deletions

View File

@ -21,7 +21,21 @@
"@bytelyst/errors": "workspace:*"
},
"peerDependencies": {
"@fastify/cors": ">=10.0.0",
"@fastify/swagger": ">=9.0.0",
"fastify": ">=5.0.0",
"@fastify/cors": ">=10.0.0"
"fastify-metrics": ">=10.0.0"
},
"peerDependenciesMeta": {
"@fastify/swagger": {
"optional": true
},
"fastify-metrics": {
"optional": true
}
},
"devDependencies": {
"@fastify/swagger": "^9.7.0",
"fastify-metrics": "^10.6.0"
}
}

View File

@ -26,7 +26,7 @@ import type { ServiceAppOptions, FastifyApp } from './types.js';
* ```
*/
export async function createServiceApp(options: ServiceAppOptions): Promise<FastifyApp> {
const { name, version, description, corsOrigin, logger = true } = options;
const { name, version, description, corsOrigin, logger = true, swagger, metrics } = options;
const app = Fastify({ logger });
@ -34,6 +34,28 @@ export async function createServiceApp(options: ServiceAppOptions): Promise<Fast
const origin = corsOrigin ? corsOrigin.split(',').map(o => o.trim()) : true;
await app.register(cors, { origin });
// OpenAPI spec (optional — consumer must have @fastify/swagger installed)
if (swagger) {
const swaggerPlugin = (await import('@fastify/swagger')).default;
await app.register(swaggerPlugin, {
openapi: {
info: {
title: swagger.title,
version,
...(swagger.description && { description: swagger.description }),
},
...(swagger.port && { servers: [{ url: `http://localhost:${swagger.port}` }] }),
},
});
}
// Prometheus metrics (optional — consumer must have fastify-metrics installed)
if (metrics) {
const metricsMod = await import('fastify-metrics');
const plugin = metricsMod.default as unknown as Parameters<typeof app.register>[0];
await app.register(plugin, { endpoint: '/metrics' });
}
// x-request-id propagation
app.addHook('onRequest', async (req, reply) => {
const requestId = (req.headers['x-request-id'] as string) || randomUUID();

View File

@ -1,11 +1,19 @@
import type { FastifyInstance } from 'fastify';
export interface SwaggerOptions {
title: string;
description?: string;
port?: number;
}
export interface ServiceAppOptions {
name: string;
version: string;
description?: string;
corsOrigin?: string;
logger?: boolean;
swagger?: SwaggerOptions;
metrics?: boolean;
}
export interface StartOptions {

19
pnpm-lock.yaml generated
View File

@ -105,6 +105,13 @@ importers:
fastify:
specifier: '>=5.0.0'
version: 5.7.4
devDependencies:
'@fastify/swagger':
specifier: ^9.7.0
version: 9.7.0
fastify-metrics:
specifier: ^10.6.0
version: 10.6.0(fastify@5.7.4)
packages/logger: {}
@ -131,6 +138,9 @@ importers:
'@bytelyst/errors':
specifier: workspace:*
version: link:../../packages/errors
'@bytelyst/fastify-core':
specifier: workspace:*
version: link:../../packages/fastify-core
'@fastify/cors':
specifier: ^10.0.2
version: 10.1.0
@ -177,6 +187,9 @@ importers:
'@bytelyst/errors':
specifier: workspace:*
version: link:../../packages/errors
'@bytelyst/fastify-core':
specifier: workspace:*
version: link:../../packages/fastify-core
'@fastify/cors':
specifier: ^10.0.2
version: 10.1.0
@ -238,6 +251,9 @@ importers:
'@bytelyst/errors':
specifier: workspace:*
version: link:../../packages/errors
'@bytelyst/fastify-core':
specifier: workspace:*
version: link:../../packages/fastify-core
'@fastify/cors':
specifier: ^10.0.2
version: 10.1.0
@ -290,6 +306,9 @@ importers:
'@bytelyst/errors':
specifier: workspace:*
version: link:../../packages/errors
'@bytelyst/fastify-core':
specifier: workspace:*
version: link:../../packages/fastify-core
'@fastify/cors':
specifier: ^10.0.2
version: 10.1.0

View File

@ -16,6 +16,7 @@
"@bytelyst/config": "workspace:*",
"@bytelyst/cosmos": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@bytelyst/fastify-core": "workspace:*",
"@azure/cosmos": "^4.2.0",
"fastify": "^5.2.1",
"@fastify/cors": "^10.0.2",

View File

@ -5,12 +5,7 @@
* Port: 4002 (configurable via PORT env var).
*/
import { randomUUID } from 'node:crypto';
import Fastify from 'fastify';
import cors from '@fastify/cors';
import swagger from '@fastify/swagger';
import metricsPlugin from 'fastify-metrics';
import { ServiceError } from './lib/errors.js';
import { createServiceApp, startService } from '@bytelyst/fastify-core';
import { subscriptionRoutes } from './modules/subscriptions/routes.js';
import { usageRoutes } from './modules/usage/routes.js';
import { planRoutes } from './modules/plans/routes.js';
@ -18,50 +13,20 @@ import { licenseRoutes } from './modules/licenses/routes.js';
import { stripeRoutes } from './modules/stripe/routes.js';
import { config } from './lib/config.js';
const PORT = config.PORT;
const HOST = config.HOST;
const app = Fastify({ logger: true });
// CORS — restrict to specific origins in production via CORS_ORIGIN (comma-separated)
const corsOrigin = config.CORS_ORIGIN;
await app.register(cors, {
origin: corsOrigin ? corsOrigin.split(',').map(o => o.trim()) : true,
});
// OpenAPI spec auto-generation (GET /api/docs/json)
await app.register(swagger, {
openapi: {
info: {
title: 'Billing & Entitlement Service',
version: '0.1.0',
description: 'Subscriptions, payments, usage, licenses, plans, Stripe',
},
servers: [{ url: `http://localhost:${PORT}` }],
},
});
// Prometheus metrics
await app.register(metricsPlugin, { endpoint: '/metrics' });
// x-request-id: propagate incoming header or generate a new one
app.addHook('onRequest', async (req, reply) => {
const requestId = (req.headers['x-request-id'] as string) || randomUUID();
req.headers['x-request-id'] = requestId;
reply.header('x-request-id', requestId);
req.log = req.log.child({ requestId });
});
// Health check
app.get('/health', async req => ({
status: 'ok',
service: 'billing-service',
const app = await createServiceApp({
name: 'billing-service',
version: '0.1.0',
timestamp: new Date().toISOString(),
requestId: req.headers['x-request-id'],
}));
description: 'Subscriptions, payments, usage, licenses, plans, Stripe',
corsOrigin: config.CORS_ORIGIN,
swagger: {
title: 'Billing & Entitlement Service',
description: 'Subscriptions, payments, usage, licenses, plans, Stripe',
port: config.PORT,
},
metrics: true,
});
// Internal API key auth (skip health, webhook, and when key not configured)
// Internal API key auth (service-specific — skip health, webhook, and when key not configured)
const INTERNAL_KEY = config.BILLING_INTERNAL_KEY;
if (INTERNAL_KEY) {
app.addHook('onRequest', async (req, reply) => {
@ -75,18 +40,6 @@ if (INTERNAL_KEY) {
});
}
// Custom error handler
app.setErrorHandler((error, _req, reply) => {
if (error instanceof ServiceError) {
const body: Record<string, unknown> = { error: error.message };
if ('details' in error && error.details) body.details = error.details;
reply.code(error.statusCode).send(body);
return;
}
app.log.error(error);
reply.code(500).send({ error: 'Internal server error' });
});
// Register route modules
await app.register(subscriptionRoutes, { prefix: '/api' });
await app.register(usageRoutes, { prefix: '/api' });
@ -94,11 +47,4 @@ await app.register(planRoutes, { prefix: '/api' });
await app.register(licenseRoutes, { prefix: '/api' });
await app.register(stripeRoutes, { prefix: '/api' });
// Start
try {
await app.listen({ port: PORT, host: HOST });
app.log.info(`Billing Service listening on ${HOST}:${PORT}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
await startService(app, { port: config.PORT, host: config.HOST });

View File

@ -16,6 +16,7 @@
"@bytelyst/config": "workspace:*",
"@bytelyst/cosmos": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@bytelyst/fastify-core": "workspace:*",
"@azure/cosmos": "^4.2.0",
"fastify": "^5.2.1",
"@fastify/cors": "^10.0.2",

View File

@ -5,66 +5,23 @@
* Port: 4001 (configurable via PORT env var).
*/
import { randomUUID } from 'node:crypto';
import Fastify from 'fastify';
import cors from '@fastify/cors';
import swagger from '@fastify/swagger';
import metricsPlugin from 'fastify-metrics';
import { ServiceError } from './lib/errors.js';
import { createServiceApp, startService } from '@bytelyst/fastify-core';
import { invitationRoutes } from './modules/invitations/routes.js';
import { referralRoutes } from './modules/referrals/routes.js';
import { promoRoutes } from './modules/promos/routes.js';
import { config } from './lib/config.js';
const PORT = config.PORT;
const HOST = config.HOST;
const app = Fastify({ logger: true });
// CORS — restrict to specific origins in production via CORS_ORIGIN (comma-separated)
const corsOrigin = config.CORS_ORIGIN;
await app.register(cors, {
origin: corsOrigin ? corsOrigin.split(',').map(o => o.trim()) : true,
});
// OpenAPI spec auto-generation (GET /api/docs/json)
await app.register(swagger, {
openapi: {
info: {
title: 'Growth Service',
version: '0.1.0',
description: 'Invitations, referrals, promo codes',
},
servers: [{ url: `http://localhost:${PORT}` }],
},
});
// Prometheus metrics
await app.register(metricsPlugin, { endpoint: '/metrics' });
// x-request-id: propagate incoming header or generate a new one
app.addHook('onRequest', async (req, reply) => {
const requestId = (req.headers['x-request-id'] as string) || randomUUID();
req.headers['x-request-id'] = requestId;
reply.header('x-request-id', requestId);
req.log = req.log.child({ requestId });
});
// Health check
app.get('/health', async req => ({
status: 'ok',
service: 'growth-service',
const app = await createServiceApp({
name: 'growth-service',
version: '0.1.0',
timestamp: new Date().toISOString(),
requestId: req.headers['x-request-id'],
}));
// Custom error handler for ServiceError
app.setErrorHandler((error, _req, reply) => {
if (error instanceof ServiceError) {
reply.code(error.statusCode).send({ error: error.message });
return;
}
app.log.error(error);
reply.code(500).send({ error: 'Internal server error' });
description: 'Invitations, referrals, promo codes',
corsOrigin: config.CORS_ORIGIN,
swagger: {
title: 'Growth Service',
description: 'Invitations, referrals, promo codes',
port: config.PORT,
},
metrics: true,
});
// Register route modules
@ -72,11 +29,4 @@ await app.register(invitationRoutes, { prefix: '/api' });
await app.register(referralRoutes, { prefix: '/api' });
await app.register(promoRoutes, { prefix: '/api' });
// Start
try {
await app.listen({ port: PORT, host: HOST });
app.log.info(`Growth Service listening on ${HOST}:${PORT}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
await startService(app, { port: config.PORT, host: config.HOST });

View File

@ -16,6 +16,7 @@
"@bytelyst/config": "workspace:*",
"@bytelyst/cosmos": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@bytelyst/fastify-core": "workspace:*",
"@azure/cosmos": "^4.2.0",
"@azure/storage-blob": "^12.31.0",
"@fastify/cors": "^10.0.2",

View File

@ -5,12 +5,7 @@
* Port: 4003 (configurable via PORT env var).
*/
import { randomUUID } from 'node:crypto';
import Fastify from 'fastify';
import cors from '@fastify/cors';
import swagger from '@fastify/swagger';
import metricsPlugin from 'fastify-metrics';
import { ServiceError } from './lib/errors.js';
import { createServiceApp, startService } from '@bytelyst/fastify-core';
import { authRoutes } from './modules/auth/routes.js';
import { auditRoutes } from './modules/audit/routes.js';
import { notificationRoutes } from './modules/notifications/routes.js';
@ -19,57 +14,17 @@ import { rateLimitRoutes } from './modules/ratelimit/routes.js';
import { blobRoutes } from './modules/blob/routes.js';
import { config } from './lib/config.js';
const PORT = config.PORT;
const HOST = config.HOST;
const app = Fastify({ logger: true });
// CORS — restrict to specific origins in production via CORS_ORIGIN (comma-separated)
const corsOrigin = config.CORS_ORIGIN;
await app.register(cors, {
origin: corsOrigin ? corsOrigin.split(',').map(o => o.trim()) : true,
});
// OpenAPI spec auto-generation (GET /api/docs/json)
await app.register(swagger, {
openapi: {
info: {
title: 'Platform Service',
version: '0.1.0',
description: 'Auth, audit, notifications, feature flags, rate limiting',
},
servers: [{ url: `http://localhost:${PORT}` }],
},
});
// Prometheus metrics
await app.register(metricsPlugin, { endpoint: '/metrics' });
// x-request-id: propagate incoming header or generate a new one
app.addHook('onRequest', async (req, reply) => {
const requestId = (req.headers['x-request-id'] as string) || randomUUID();
req.headers['x-request-id'] = requestId;
reply.header('x-request-id', requestId);
req.log = req.log.child({ requestId });
});
// Health check
app.get('/health', async req => ({
status: 'ok',
service: 'platform-service',
const app = await createServiceApp({
name: 'platform-service',
version: '0.1.0',
timestamp: new Date().toISOString(),
requestId: req.headers['x-request-id'],
}));
// Custom error handler
app.setErrorHandler((error, _req, reply) => {
if (error instanceof ServiceError) {
reply.code(error.statusCode).send({ error: error.message });
return;
}
app.log.error(error);
reply.code(500).send({ error: 'Internal server error' });
description: 'Auth, audit, notifications, feature flags, rate limiting',
corsOrigin: config.CORS_ORIGIN,
swagger: {
title: 'Platform Service',
description: 'Auth, audit, notifications, feature flags, rate limiting',
port: config.PORT,
},
metrics: true,
});
// Register route modules
@ -80,11 +35,4 @@ await app.register(flagRoutes, { prefix: '/api' });
await app.register(rateLimitRoutes, { prefix: '/api' });
await app.register(blobRoutes, { prefix: '/api' });
// Start
try {
await app.listen({ port: PORT, host: HOST });
app.log.info(`Platform Service listening on ${HOST}:${PORT}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
await startService(app, { port: config.PORT, host: config.HOST });

View File

@ -16,6 +16,7 @@
"@bytelyst/config": "workspace:*",
"@bytelyst/cosmos": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@bytelyst/fastify-core": "workspace:*",
"@azure/cosmos": "^4.2.0",
"@fastify/cors": "^10.0.2",
"@fastify/rate-limit": "^10.3.0",

View File

@ -6,69 +6,24 @@
* Product-agnostic: all data scoped by productId.
*/
import { randomUUID } from 'node:crypto';
import Fastify from 'fastify';
import cors from '@fastify/cors';
import swagger from '@fastify/swagger';
import metricsPlugin from 'fastify-metrics';
import { ServiceError } from './lib/errors.js';
import { createServiceApp, startService } from '@bytelyst/fastify-core';
import { itemRoutes } from './modules/items/routes.js';
import { commentRoutes } from './modules/comments/routes.js';
import { voteRoutes } from './modules/votes/routes.js';
import { publicRoutes } from './modules/public/routes.js';
import { config } from './lib/config.js';
const PORT = config.PORT;
const HOST = config.HOST;
const app = Fastify({ logger: true });
// CORS — restrict to specific origins in production via CORS_ORIGIN (comma-separated)
const corsOrigin = config.CORS_ORIGIN;
await app.register(cors, {
origin: corsOrigin ? corsOrigin.split(',').map(o => o.trim()) : true,
});
// OpenAPI spec auto-generation (GET /api/docs/json)
await app.register(swagger, {
openapi: {
info: {
title: 'Tracker Service',
version: '0.1.0',
description: 'Feature requests, bugs, tasks — product-agnostic',
},
servers: [{ url: `http://localhost:${PORT}` }],
},
});
// Prometheus metrics
await app.register(metricsPlugin, { endpoint: '/metrics' });
// x-request-id: propagate incoming header or generate a new one
app.addHook('onRequest', async (req, reply) => {
const requestId = (req.headers['x-request-id'] as string) || randomUUID();
req.headers['x-request-id'] = requestId;
reply.header('x-request-id', requestId);
req.log = req.log.child({ requestId });
});
// Health check
app.get('/health', async req => ({
status: 'ok',
service: 'tracker-service',
const app = await createServiceApp({
name: 'tracker-service',
version: '0.1.0',
timestamp: new Date().toISOString(),
requestId: req.headers['x-request-id'],
}));
// Custom error handler
app.setErrorHandler((error, _req, reply) => {
if (error instanceof ServiceError) {
reply.code(error.statusCode).send({ error: error.message });
return;
}
app.log.error(error);
reply.code(500).send({ error: 'Internal server error' });
description: 'Feature requests, bugs, tasks — product-agnostic',
corsOrigin: config.CORS_ORIGIN,
swagger: {
title: 'Tracker Service',
description: 'Feature requests, bugs, tasks — product-agnostic',
port: config.PORT,
},
metrics: true,
});
// Register route modules
@ -77,11 +32,4 @@ await app.register(commentRoutes, { prefix: '/api' });
await app.register(voteRoutes, { prefix: '/api' });
await app.register(publicRoutes, { prefix: '/api' });
// Start
try {
await app.listen({ port: PORT, host: HOST });
app.log.info(`Tracker Service listening on ${HOST}:${PORT}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
await startService(app, { port: config.PORT, host: config.HOST });