@cse-350/shared-library

v1.0.11

Published

Shared library for SkillTrade project

Readme

@cse-350/shared-library

A comprehensive TypeScript shared library for the SkillTrade microservices ecosystem, providing common utilities, event-driven communication, error handling, and middleware functionality.

npm version TypeScript

šŸ“‹ Table of Contents

Overview

The @cse-350/shared-library is a foundational package that enables consistent communication and functionality across all SkillTrade microservices. It provides:

  • Event-driven architecture using NATS Streaming
  • Standardized error handling with custom error classes
  • Common middleware for authentication, validation, and error processing
  • Type definitions for consistent data structures
  • Publisher/Subscriber patterns for service communication

This library ensures consistency, reduces code duplication, and maintains type safety across the entire microservices ecosystem.

Installation

npm install @cse-350/shared-library

Peer Dependencies

npm install express jsonwebtoken cookie-session express-validator node-nats-streaming

Architecture

The shared library is organized into four main modules:

src/
ā”œā”€ā”€ events/           # Event-driven communication system
│   ā”œā”€ā”€ subjects.ts   # Event subject definitions
│   ā”œā”€ā”€ base-listener.ts
│   ā”œā”€ā”€ base-publisher.ts
│   ā”œā”€ā”€ *-event.ts    # Specific event interfaces
│   └── types/        # Common types
ā”œā”€ā”€ errors/           # Custom error classes
ā”œā”€ā”€ middlewares/      # Express middleware functions
└── index.ts         # Main export file

API Reference

Events System

Subjects

All event subjects are defined in a centralized enum:

export enum Subjects {
  PaymentCreated = "payment:created",
  ConnectionRequested = "connection:requested",
  ConnectionRejected = "connection:rejected",
  ConnectionAccepted = "connection:accepted",
  ConnectionCancelled = "connection:cancelled",
  PostDeleted = "post:deleted",
  ReviewCreated = "review:created",
}

Base Publisher

Abstract class for publishing events to NATS Streaming:

export abstract class Publisher<T extends Event> {
  abstract subject: T['subject'];
  protected client: Stan;
  
  constructor(client: Stan);
  publish(data: T['data']): Promise<void>;
}

Base Listener

Abstract class for listening to events from NATS Streaming:

export abstract class Listener<T extends Event> {
  abstract subject: T['subject'];
  abstract queueGroupName: string;
  abstract onMessage(data: T['data'], msg: Message): void;
  protected client: Stan;
  protected ackWait: number = 5000;
  
  constructor(client: Stan);
  listen(): void;
}

Event Interfaces

ConnectionRequestedEvent
export interface ConnectionRequestedEvent {
  subject: Subjects.ConnectionRequested;
  data: {
    postId: string;
    postTitle: string;
    postAuthorId: string;
    requestedUserId: string;
    requestedUserName: string;
    requestedUserProfilePicture: string;
    toLearn: string[];
    toTeach: string[];
  };
}
PaymentCreatedEvent
export interface PaymentCreatedEvent {
  subject: Subjects.PaymentCreated;
  data: {
    userId: string;
    stripeId: string;
  };
}
ReviewCreatedEvent
export interface ReviewCreatedEvent {
  subject: Subjects.ReviewCreated;
  data: {
    userId: string;
    review: string;
    rating: number;
  };
}

Error Handling

CustomError (Base Class)

export abstract class CustomError extends Error {
  abstract statusCode: number;
  abstract serializeErrors(): {
    message: string;
    field?: string;
  }[];
}

Specific Error Classes

BadRequestError
export class BadRequestError extends CustomError {
  statusCode = 400;
  constructor(public message: string);
  serializeErrors(): { message: string; field?: string }[];
}
NotFoundError
export class NotFoundError extends CustomError {
  statusCode = 404;
  constructor();
  serializeErrors(): { message: string; field?: string }[];
}
NotAuthenticatedError
export class NotAuthenticatedError extends CustomError {
  statusCode = 401;
  constructor();
  serializeErrors(): { message: string; field?: string }[];
}
RequestValidationError
export class RequestValidationError extends CustomError {
  statusCode = 400;
  constructor(public errs: ValidationError[]);
  serializeErrors(): { message: string; field?: string }[];
}
DatabaseError
export class DatabaseError extends CustomError {
  statusCode = 500;
  constructor(public reason: string = 'Error connecting to database');
  serializeErrors(): { message: string; field?: string }[];
}

Middleware

errorHandler

Global error handling middleware that processes all custom errors:

export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) => void;

Features:

  • Handles custom errors with appropriate status codes
  • Logs unhandled errors with stack traces
  • Returns standardized error responses

setCurrentUser

JWT authentication middleware that decodes and sets the current user:

export const setCurrentUser = (
  req: Request,
  res: Response,
  next: NextFunction
) => void;

Features:

  • Decodes JWT from session cookies
  • Sets req.currentUser with user information
  • Gracefully handles invalid tokens

requireAuth

Authorization middleware that ensures user authentication:

export const requireAuth = (
  req: Request,
  res: Response,
  next: NextFunction
) => void;

Features:

  • Throws NotAuthenticatedError if user is not authenticated
  • Must be used after setCurrentUser

requestValidationHandler

Express-validator error processing middleware:

export const requestValidationHandler = (
  req: Request,
  res: Response,
  next: NextFunction
) => void;

Features:

  • Processes express-validator results
  • Throws RequestValidationError for validation failures

Types

User Interface (from setCurrentUser)

interface decodedUser {
  email: string;
  id: string;
  profilePicture: string;
  fullName: string;
  isPremium: boolean;
}

Order Status Enum

export enum OrderStatus {
  Created = 'created',
  Cancelled = 'cancelled',
  AwatingPayment = 'awating:payment',
  Complete = 'complete',
}

Usage Examples

Creating a Publisher

import { Publisher, PaymentCreatedEvent, Subjects } from '@cse-350/shared-library';
import { natsWrapper } from '../nats-wrapper';

class PaymentCreatedPublisher extends Publisher<PaymentCreatedEvent> {
  subject: Subjects.PaymentCreated = Subjects.PaymentCreated;
}

// Usage
const publisher = new PaymentCreatedPublisher(natsWrapper.client);
await publisher.publish({
  userId: 'user123',
  stripeId: 'stripe456'
});

Creating a Listener

import { Listener, PaymentCreatedEvent, Subjects } from '@cse-350/shared-library';
import { Message } from 'node-nats-streaming';

class PaymentCreatedListener extends Listener<PaymentCreatedEvent> {
  subject: Subjects.PaymentCreated = Subjects.PaymentCreated;
  queueGroupName = 'auth-service';

  async onMessage(data: PaymentCreatedEvent['data'], msg: Message) {
    // Update user premium status
    const user = await User.findById(data.userId);
    if (user) {
      user.isPremium = true;
      await user.save();
    }
    
    msg.ack();
  }
}

Using Middleware

import express from 'express';
import { 
  errorHandler, 
  setCurrentUser, 
  requireAuth,
  requestValidationHandler,
  BadRequestError
} from '@cse-350/shared-library';
import { body } from 'express-validator';

const app = express();

// Global middleware
app.use(express.json());
app.use(setCurrentUser);

// Route with validation and authentication
app.post('/api/posts', 
  [
    body('title').notEmpty().withMessage('Title is required'),
    body('content').notEmpty().withMessage('Content is required')
  ],
  requestValidationHandler,
  requireAuth,
  async (req, res) => {
    const { title, content } = req.body;
    
    if (!title || !content) {
      throw new BadRequestError('Missing required fields');
    }
    
    // Create post logic
    res.status(201).json({ message: 'Post created' });
  }
);

// Global error handler (must be last)
app.use(errorHandler);

Error Handling

import { BadRequestError, NotFoundError } from '@cse-350/shared-library';

// In your route handlers
app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  
  if (!user) {
    throw new NotFoundError();
  }
  
  res.json(user);
});

app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  
  if (!email || !password) {
    throw new BadRequestError('Email and password are required');
  }
  
  // Login logic
});

Event-Driven Communication

Communication Flow

  1. Service A publishes an event using a Publisher
  2. NATS Streaming receives and stores the event
  3. Service B receives the event through a Listener
  4. Service B processes the event and acknowledges it

Event Patterns

Payment Processing

Payment Service → PaymentCreated → Auth Service
                               → Community Service

Connection Management

Community Service → ConnectionRequested → Connection Service
Connection Service → ConnectionAccepted → Community Service
                                      → Auth Service

Post Management

Community Service → PostDeleted → Connection Service

Review System

Connection Service → ReviewCreated → Auth Service

Message Acknowledgment

All listeners must acknowledge messages to ensure delivery:

async onMessage(data: EventData, msg: Message) {
  try {
    // Process the event
    await this.processEvent(data);
    
    // Acknowledge successful processing
    msg.ack();
  } catch (error) {
    console.error('Error processing event:', error);
    // Don't acknowledge - message will be redelivered
  }
}

Best Practices

Event Design

  1. Immutable Events: Events should represent facts that have already happened
  2. Minimal Data: Include only essential data in events
  3. Backward Compatibility: Ensure new event versions don't break existing listeners

Error Handling

  1. Use Specific Errors: Choose the most appropriate error class
  2. Provide Clear Messages: Error messages should be user-friendly
  3. Log Detailed Errors: Include stack traces for debugging

Middleware Usage

  1. Order Matters: Apply middleware in the correct sequence
  2. Authentication First: Use setCurrentUser before requireAuth
  3. Validation Early: Validate requests before business logic

Type Safety

  1. Extend Interfaces: Use proper TypeScript interfaces for events
  2. Generic Types: Leverage generics for reusable components
  3. Strict Typing: Enable strict TypeScript checking

Contributing

Development Setup

# Clone the repository
git clone <repository-url>
cd shared

# Install dependencies
npm install

# Build the library
npm run build

# Publish (requires permissions)
npm run pub

Publishing Process

# Update version and publish
npm run pub

This will:

  1. Add changes to git
  2. Commit with "Updated modules" message
  3. Bump patch version
  4. Build the TypeScript
  5. Publish to npm

Testing

The shared library is tested through integration with the microservices. When making changes:

  1. Update the library
  2. Publish a new version
  3. Update dependencies in all services
  4. Run service test suites

Version History

  • v1.0.10: Current stable version
  • v1.0.x: Initial release with core functionality

Dependencies

{
  "dependencies": {
    "@types/cookie-session": "^2.0.49",
    "@types/express": "^5.0.1",
    "@types/jsonwebtoken": "^9.0.9",
    "cookie-session": "^2.1.0",
    "express": "^5.1.0",
    "express-validator": "^7.2.1",
    "jsonwebtoken": "^9.0.2",
    "node-nats-streaming": "^0.3.2"
  }
}

License

ISC License - see package.json for details.

Support

For issues or questions:


Package URL: https://www.npmjs.com/package/@cse-350/shared-library

This shared library is the foundation that enables the SkillTrade microservices ecosystem to function cohesively while maintaining independence and scalability.