Skip to main content

Overview

The @trendingsociety/api package provides standardized utilities for API development:
  • Response formatting
  • Error handling
  • Structured logging
  • Handler factories

Installation

pnpm add @trendingsociety/api

Response Utilities

successResponse()

Creates a standardized success response.
import { successResponse } from '@trendingsociety/api';

export async function GET() {
  const data = await fetchData();

  return successResponse(data);
  // { success: true, data: {...} }
}
With metadata:
return successResponse(data, {
  meta: {
    page: 1,
    total: 100,
    hasMore: true
  }
});

errorResponse()

Creates a standardized error response.
import { errorResponse } from '@trendingsociety/api';

export async function GET() {
  if (!authorized) {
    return errorResponse('Unauthorized', 401, 'AUTH_REQUIRED');
  }
}
// { success: false, error: 'Unauthorized', code: 'AUTH_REQUIRED' }

Error Classes

Pre-built error classes for common scenarios:
import {
  ValidationError,
  NotFoundError,
  UnauthorizedError,
  ForbiddenError,
  RateLimitError
} from '@trendingsociety/api';

// Throws with proper status code and error code
throw new ValidationError('Email is required');
throw new NotFoundError('User not found');
throw new UnauthorizedError('Invalid token');
throw new ForbiddenError('Insufficient permissions');
throw new RateLimitError('Too many requests');
Usage in API routes:
import { NotFoundError, errorResponse } from '@trendingsociety/api';

export async function GET(request: Request, { params }) {
  try {
    const user = await getUser(params.id);
    if (!user) throw new NotFoundError('User not found');
    return successResponse(user);
  } catch (error) {
    if (error instanceof NotFoundError) {
      return errorResponse(error.message, 404, 'NOT_FOUND');
    }
    throw error;
  }
}

Logger

Structured logging with context:
import { logger } from '@trendingsociety/api';

logger.info('User created', { userId: '123', tenantId: 'acme' });
logger.warn('Rate limit approaching', { remaining: 5 });
logger.error('Database connection failed', { error: err.message });
Log levels:
  • debug - Development only
  • info - Standard operations
  • warn - Potential issues
  • error - Failures

Handler Factories

Create reusable handlers that work across apps:
// packages/api/src/shopify/handlers.ts
export function createKPIsHandler(getTenantId: () => Promise<string>) {
  return async function handler(request: Request) {
    const tenantId = await getTenantId();

    const credentials = await getShopifyCredentials(tenantId);
    const kpis = await fetchShopifyKPIs(credentials);

    return successResponse(kpis);
  };
}

export function createProductsHandler(getTenantId: () => Promise<string>) {
  return async function handler(request: Request) {
    const tenantId = await getTenantId();
    const url = new URL(request.url);
    const limit = parseInt(url.searchParams.get('limit') || '10');

    const products = await fetchProducts(tenantId, { limit });

    return successResponse(products);
  };
}
Usage in apps:
// apps/dashboard/src/app/api/shopify/kpis/route.ts
import { createKPIsHandler } from '@trendingsociety/api/shopify';
import { getTenantId } from '@/lib/tenant/server';

export const GET = createKPIsHandler(getTenantId);
// apps/agency/src/app/api/shopify/kpis/route.ts
import { createKPIsHandler } from '@trendingsociety/api/shopify';
import { getTenantId } from '@/lib/tenant/server';

// Same handler, different app!
export const GET = createKPIsHandler(getTenantId);

Circuit Breaker

Protect against cascading failures:
import { createCircuitBreaker } from '@trendingsociety/api';

const shopifyBreaker = createCircuitBreaker({
  name: 'shopify',
  threshold: 5,        // failures before open
  resetTimeout: 30000  // ms before half-open
});

const result = await shopifyBreaker.call(async () => {
  return fetch('https://shop.myshopify.com/admin/api/...');
});

N+1 Prevention

Use DataLoader pattern for batched queries:
import { createDataLoader } from '@trendingsociety/api';

const userLoader = createDataLoader(async (ids: string[]) => {
  const { data } = await supabase
    .from('users')
    .select('*')
    .in('id', ids);
  return data;
});

// These are batched into one query
const user1 = await userLoader.load('user-1');
const user2 = await userLoader.load('user-2');

Full Example

import {
  successResponse,
  errorResponse,
  ValidationError,
  NotFoundError,
  logger
} from '@trendingsociety/api';

export async function POST(request: Request) {
  try {
    const body = await request.json();

    // Validate
    if (!body.email) {
      throw new ValidationError('Email is required');
    }

    // Process
    logger.info('Creating user', { email: body.email });
    const user = await createUser(body);

    // Success
    return successResponse(user, { status: 201 });

  } catch (error) {
    logger.error('User creation failed', { error: error.message });

    if (error instanceof ValidationError) {
      return errorResponse(error.message, 400, 'VALIDATION_ERROR');
    }
    if (error instanceof NotFoundError) {
      return errorResponse(error.message, 404, 'NOT_FOUND');
    }

    return errorResponse('Internal server error', 500, 'INTERNAL_ERROR');
  }
}