npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gfed-medusa/bff-lib-common

v1.1.1

Published

Shared observability utilities for GFED Medusa BFF

Readme

@gfed-medusa/bff-lib-common

Shared observability utilities for the GFED Medusa BFF monorepo, including logging, error handling, and health checks.

Installation

Install the package from npm:

pnpm add @gfed-medusa/bff-lib-common

For monorepo or local development (to use what's latest), you can use workspace dependencies:

{
  "dependencies": {
    "@gfed-medusa/bff-lib-common": "workspace:*"
  }
}

Usage

Logger

Create a structured logger for your subgraph:

import {
  type LogLevel,
  createChildLogger,
  createLogger,
} from '@gfed-medusa/bff-lib-common';

// Initialize logger for your subgraph
const logger = createLogger({
  serviceName: 'products-subgraph',
  level: (process.env.LOG_LEVEL as LogLevel) || 'info',
  pretty: process.env.NODE_ENV === 'development',
});

// Basic logging
logger.info('Products subgraph starting');
logger.info({ port: 4001 }, 'Server listening on port 4001');

// Logging in resolvers
const resolvers = {
  Query: {
    products: () => {
      logger.info('Fetching all products');
      return productService.getAll();
    },
    product: (_: unknown, { id }: { id: string }) => {
      logger.info({ productId: id }, 'Fetching product by ID');
      return productService.getById(id);
    },
  },
};

// Child logger with request context
app.use((req, res, next) => {
  req.logger = createChildLogger(logger, {
    requestId: req.headers['x-request-id'] || crypto.randomUUID(),
    path: req.path,
  });
  next();
});

Error Handling

Handle errors consistently across your subgraph:

import {
  NotFoundError,
  ValidationError,
  asyncHandler,
  createErrorHandler,
} from '@gfed-medusa/bff-lib-common';

// In your GraphQL resolvers
const resolvers = {
  Query: {
    product: (_: unknown, { id }: { id: string }) => {
      if (!id || id.trim() === '') {
        throw new ValidationError('Product ID is required');
      }

      const product = productService.getById(id);
      if (!product) {
        throw new NotFoundError('Product not found', { productId: id });
      }

      return product;
    },
  },
};

Health Checks

Add health check endpoints for monitoring:

import { HealthCheck, checkHttpEndpoint } from '@gfed-medusa/bff-lib-common';

// Create health check with custom checks
const healthCheck = new HealthCheck('products-subgraph', '1.0.0');

// Check database connection
healthCheck.register('database', async () => {
  try {
    await productService.healthCheck(); // or db.ping()
    return { status: 'healthy' };
  } catch (error) {
    return {
      status: 'unhealthy',
      message: error instanceof Error ? error.message : 'Database check failed',
    };
  }
});

// Check external Medusa API
healthCheck.register('medusa-api', async () => {
  return await checkHttpEndpoint(
    process.env.MEDUSA_API_URL + '/health',
    5000 // 5 second timeout
  );
});

// Add health endpoints
app.get('/health', healthCheck.getHandler());

// Simple liveness probe (always returns healthy if app is running)
app.get('/health/live', (req, res) => {
  res.status(200).json({
    status: 'healthy',
    service: 'products-subgraph',
    timestamp: new Date().toISOString(),
  });
});

Complete Integration Examples

Products Subgraph with Full Observability

Here's a complete, production-ready example of the products subgraph with all observability features:

import cors from 'cors';
import express from 'express';
import gql from 'graphql-tag';
import http from 'http';

import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { expressMiddleware } from '@as-integrations/express5';
import {
  HealthCheck,
  NotFoundError,
  ValidationError,
  checkHttpEndpoint,
  createErrorHandler,
  createLogger,
} from '@gfed-medusa/bff-lib-common';

// Initialize logger
const logger = createLogger({
  serviceName: 'products-subgraph',
  level: process.env.LOG_LEVEL || 'info',
  pretty: process.env.NODE_ENV === 'development',
});

// GraphQL Schema for Medusa products
const typeDefs = gql`
  type Product @key(fields: "id") {
    id: ID!
    title: String!
    description: String
    thumbnail: String
    handle: String!
    status: ProductStatus!
  }

  enum ProductStatus {
    draft
    published
    rejected
  }

  type Query {
    products(limit: Int = 10, offset: Int = 0): [Product!]!
    product(id: ID!): Product
    productByHandle(handle: String!): Product
  }
`;

// Product service (simulated - replace with actual Medusa API calls)
const productService = {
  async getAll(limit: number, offset: number) {
    logger.info({ limit, offset }, 'Fetching products from Medusa');
    // Replace with: await medusaClient.products.list({ limit, offset })
    return [
      {
        id: 'prod_1',
        title: 'T-Shirt',
        handle: 't-shirt',
        status: 'published',
      },
      { id: 'prod_2', title: 'Hoodie', handle: 'hoodie', status: 'published' },
    ];
  },

  async getById(id: string) {
    logger.info({ productId: id }, 'Fetching product by ID');
    // Replace with: await medusaClient.products.retrieve(id)
    const products = await this.getAll(100, 0);
    return products.find((p) => p.id === id);
  },

  async getByHandle(handle: string) {
    logger.info({ handle }, 'Fetching product by handle');
    // Replace with: await medusaClient.products.list({ handle })
    const products = await this.getAll(100, 0);
    return products.find((p) => p.handle === handle);
  },
};

const resolvers = {
  Query: {
    products: async (
      _: unknown,
      { limit, offset }: { limit: number; offset: number }
    ) => {
      if (limit < 1 || limit > 100) {
        throw new ValidationError('Limit must be between 1 and 100', { limit });
      }
      if (offset < 0) {
        throw new ValidationError('Offset must be non-negative', { offset });
      }

      return await productService.getAll(limit, offset);
    },

    product: async (_: unknown, { id }: { id: string }) => {
      if (!id || id.trim() === '') {
        throw new ValidationError('Product ID is required');
      }

      const product = await productService.getById(id);
      if (!product) {
        throw new NotFoundError('Product not found', { productId: id });
      }

      return product;
    },

    productByHandle: async (_: unknown, { handle }: { handle: string }) => {
      if (!handle || handle.trim() === '') {
        throw new ValidationError('Product handle is required');
      }

      const product = await productService.getByHandle(handle);
      if (!product) {
        throw new NotFoundError('Product not found', { handle });
      }

      return product;
    },
  },
};

// Health checks
const healthCheck = new HealthCheck('products-subgraph', '1.0.0');

// Check Medusa API connection
healthCheck.register('medusa-api', async () => {
  const medusaUrl = process.env.MEDUSA_API_URL || 'http://localhost:9000';
  return await checkHttpEndpoint(`${medusaUrl}/health`, 5000);
});

// Check database if using direct DB connection
healthCheck.register('database', async () => {
  try {
    // await db.raw('SELECT 1')
    return { status: 'healthy' };
  } catch (error) {
    return {
      status: 'unhealthy',
      message: error instanceof Error ? error.message : 'Database check failed',
    };
  }
});

async function startServer() {
  const app = express();
  const httpServer = http.createServer(app);

  app.use(cors<cors.CorsRequest>());
  app.use(express.json());

  // Health endpoints
  app.get('/health', healthCheck.getHandler());
  app.get('/health/live', (req, res) => {
    res.status(200).json({
      status: 'healthy',
      service: 'products-subgraph',
      timestamp: new Date().toISOString(),
    });
  });

  // Apollo Server
  const server = new ApolloServer({
    schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
    plugins: [
      ApolloServerPluginDrainHttpServer({ httpServer }),
      ...(process.env.NODE_ENV === 'development'
        ? [ApolloServerPluginLandingPageLocalDefault()]
        : []),
      {
        async requestDidStart() {
          const start = Date.now();
          return {
            async willSendResponse(requestContext) {
              const duration = Date.now() - start;
              logger.info(
                {
                  operation: requestContext.request.operationName,
                  duration,
                },
                'GraphQL request completed'
              );
            },
            async didEncounterErrors(requestContext) {
              for (const error of requestContext.errors) {
                logger.error(
                  {
                    err: error,
                    operation: requestContext.request.operationName,
                    variables: requestContext.request.variables,
                  },
                  'GraphQL error occurred'
                );
              }
            },
          };
        },
      },
    ],
    introspection: process.env.NODE_ENV !== 'production',
  });

  await server.start();
  app.use('/graphql', expressMiddleware(server));

  // Error handler (must be last)
  app.use(createErrorHandler(logger));

  const port = process.env.PORT || 4001;
  await new Promise<void>((resolve) => httpServer.listen({ port }, resolve));

  logger.info(
    { port, env: process.env.NODE_ENV },
    `Products subgraph ready at http://localhost:${port}/graphql`
  );

  // Graceful shutdown
  process.on('SIGTERM', () => {
    logger.info('SIGTERM signal received: closing HTTP server');
    httpServer.close(() => {
      logger.info('HTTP server closed');
      process.exit(0);
    });
  });
}

startServer().catch((error) => {
  logger.error({ err: error }, 'Failed to start products subgraph');
  process.exit(1);
});

Gateway Integration

Example of using observability in the Apollo Gateway:

import express from 'express';
import http from 'http';

import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { expressMiddleware } from '@as-integrations/express5';
import {
  HealthCheck,
  checkHttpEndpoint,
  createErrorHandler,
  createLogger,
} from '@gfed-medusa/bff-lib-common';

const logger = createLogger({
  serviceName: 'gateway',
  level: process.env.LOG_LEVEL || 'info',
  pretty: process.env.NODE_ENV === 'development',
});

// Health checks for all subgraphs
const healthCheck = new HealthCheck('gateway', '1.0.0');

healthCheck.register('products-subgraph', async () => {
  return await checkHttpEndpoint(process.env.PRODUCTS_URL + '/health', 5000);
});

healthCheck.register('identity-subgraph', async () => {
  return await checkHttpEndpoint(process.env.IDENTITY_URL + '/health', 5000);
});

healthCheck.register('content-subgraph', async () => {
  return await checkHttpEndpoint(process.env.CONTENT_URL + '/health', 5000);
});

healthCheck.register('orders-subgraph', async () => {
  return await checkHttpEndpoint(process.env.ORDERS_URL + '/health', 5000);
});

async function startGateway() {
  const app = express();
  const httpServer = http.createServer(app);

  const gateway = new ApolloGateway({
    supergraphSdl: new IntrospectAndCompose({
      subgraphs: [
        {
          name: 'products',
          url: process.env.PRODUCTS_URL || 'http://localhost:4001/graphql',
        },
        {
          name: 'identity',
          url: process.env.IDENTITY_URL || 'http://localhost:4002/graphql',
        },
        {
          name: 'content',
          url: process.env.CONTENT_URL || 'http://localhost:4003/graphql',
        },
        {
          name: 'orders',
          url: process.env.ORDERS_URL || 'http://localhost:4004/graphql',
        },
      ],
    }),
  });

  const server = new ApolloServer({
    gateway,
    plugins: [
      ApolloServerPluginDrainHttpServer({ httpServer }),
      {
        async requestDidStart() {
          const start = Date.now();
          return {
            async willSendResponse(requestContext) {
              const duration = Date.now() - start;
              logger.info(
                {
                  operation: requestContext.request.operationName,
                  duration,
                },
                'Gateway request completed'
              );
            },
            async didEncounterErrors(requestContext) {
              for (const error of requestContext.errors) {
                logger.error(
                  {
                    err: error,
                    operation: requestContext.request.operationName,
                  },
                  'Gateway error occurred'
                );
              }
            },
          };
        },
      },
    ],
  });

  await server.start();

  app.use(express.json());
  app.get('/health', healthCheck.getHandler());
  app.use('/graphql', expressMiddleware(server));
  app.use(createErrorHandler(logger));

  const port = process.env.PORT || 4000;
  await new Promise<void>((resolve) => httpServer.listen({ port }, resolve));

  logger.info(
    { port },
    'Gateway ready at http://localhost:' + port + '/graphql'
  );

  process.on('SIGTERM', () => {
    logger.info('Shutting down gateway');
    httpServer.close(() => process.exit(0));
  });
}

startGateway().catch((error) => {
  logger.error({ err: error }, 'Failed to start gateway');
  process.exit(1);
});

Environment Variables

# Logging
LOG_LEVEL=info

# Server
PORT=4001
NODE_ENV=development

Publishing

To publish to npm:

cd packages/lib-common
npm version patch  # or minor, major
npm publish

API Reference

Exported Types

LogLevel

Type for log levels:

type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';

Usage:

import type { LogLevel } from '@gfed-medusa/bff-lib-common';

const level = (process.env.LOG_LEVEL as LogLevel) || 'info';

Logger

createLogger(config: LoggerConfig): Logger

Creates a Pino logger instance.

Config Options:

  • serviceName: Name of the service
  • level: Log level (default: 'info') - one of: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
  • pretty: Enable pretty printing for development (default: false)
  • redactPaths: Additional paths to redact from logs

createChildLogger(logger: Logger, context: Record<string, unknown>): Logger

Creates a child logger with additional context.

Errors

Error Classes

  • AppError: Base error class
  • ValidationError: 400 validation errors
  • NotFoundError: 404 not found errors
  • UnauthorizedError: 401 unauthorized errors
  • ForbiddenError: 403 forbidden errors
  • ConflictError: 409 conflict errors
  • ServiceUnavailableError: 503 service unavailable errors

createErrorHandler(logger: Logger): ErrorRequestHandler

Creates Express error handling middleware.

asyncHandler(fn: AsyncRequestHandler): RequestHandler

Wraps async route handlers to catch promise rejections.

Health Checks

HealthCheck

Class for managing multiple health checks.

Methods:

  • register(name: string, checker: HealthChecker): void
  • execute(): Promise<HealthCheckResult>
  • getHandler(): RequestHandler

createSimpleHealthCheck(serviceName: string, version?: string): RequestHandler

Creates a simple health check endpoint that always returns healthy.

checkHttpEndpoint(url: string, timeout?: number): Promise<HealthCheckResult>

Helper to check if an HTTP endpoint is healthy.