Skip to main content

Service Layer

Overview

The service layer contains all business logic for the BookWish backend. Services are responsible for:

  • Database operations via Prisma
  • Business rule enforcement
  • Data transformation
  • Third-party API integration
  • Transaction management
  • Background job queuing

Service Architecture

Separation of Concerns

Controller → Service → Database/External APIs

Jobs/Queue
  • Controllers: HTTP request/response handling
  • Services: Business logic and orchestration
  • Prisma: Database access
  • Jobs: Asynchronous processing

Service Pattern

import { prisma } from '../config/database';
import { logger } from '../lib/logger';

export interface CreateData {
name: string;
description?: string;
}

export interface UpdateData {
name?: string;
description?: string;
}

/**
* Get resource by ID
*/
export async function getById(id: string) {
const resource = await prisma.resource.findUnique({
where: { id },
include: {
relatedModel: true,
},
});

return resource;
}

/**
* Create new resource
*/
export async function create(userId: string, data: CreateData) {
// Business logic validation
if (!data.name) {
throw { status: 400, error: 'BadRequest', message: 'Name is required' };
}

// Create resource
const resource = await prisma.resource.create({
data: {
userId,
name: data.name,
description: data.description,
},
});

logger.info('resource.created', { resourceId: resource.id, userId });

return resource;
}

/**
* Update resource (with ownership check)
*/
export async function update(id: string, userId: string, data: UpdateData) {
// Check ownership
const existing = await prisma.resource.findUnique({
where: { id },
});

if (!existing) {
throw { status: 404, error: 'NotFound', message: 'Resource not found' };
}

if (existing.userId !== userId) {
throw { status: 403, error: 'Forbidden', message: 'Access denied' };
}

// Update resource
const updated = await prisma.resource.update({
where: { id },
data: {
name: data.name,
description: data.description,
},
});

logger.info('resource.updated', { resourceId: id, userId });

return updated;
}

/**
* Delete resource (with ownership check)
*/
export async function deleteResource(id: string, userId: string) {
// Check ownership
const existing = await prisma.resource.findUnique({
where: { id },
});

if (!existing) {
throw { status: 404, error: 'NotFound', message: 'Resource not found' };
}

if (existing.userId !== userId) {
throw { status: 403, error: 'Forbidden', message: 'Access denied' };
}

// Delete resource
await prisma.resource.delete({
where: { id },
});

logger.info('resource.deleted', { resourceId: id, userId });
}

Core Services

Authentication Service (auth.service.ts)

Handles user authentication and session management.

Key functions:

  • createGuestUser(deviceId) - Create guest account
  • signup(email, password, displayName) - User registration
  • login(email, password) - User login
  • refreshTokens(refreshToken) - Refresh JWT tokens
  • migrateGuest(guestUserId, email, password, displayName) - Convert guest to full account
  • logout(userId, refreshToken) - Logout and blacklist token

Features:

  • JWT token generation (access + refresh)
  • Password hashing with bcrypt
  • Token blacklisting in Redis
  • Guest migration with data preservation

Book Service (book.service.ts)

Book metadata and discovery.

Key functions:

  • searchBooks(query, options) - Search books by title/author/ISBN
  • getBookById(id) - Get book details
  • getBookByISBN(isbn) - Get or create book by ISBN
  • checkAvailability(bookId, userLocation) - Check store availability

Features:

  • Google Books API integration
  • ISBNdb API fallback
  • Book metadata caching
  • Availability checking across stores

Inventory Service (inventory.service.ts)

Store inventory management and synchronization.

Key functions:

  • getStoreInventory(storeId, query) - List store inventory
  • addInventory(storeId, data) - Add inventory item
  • updateInventory(id, data) - Update inventory
  • deleteInventory(id) - Remove inventory
  • syncFromSquare(storeId) - Sync inventory from Square POS
  • reserveInventory(inventoryId, quantity) - Reserve for order
  • releaseReservation(inventoryId, quantity) - Release reservation

Features:

  • Square POS synchronization
  • Stock reservations for orders
  • Stock alert notifications
  • Bulk CSV import

Order Service (order.service.ts)

Order management and fulfillment.

Key functions:

  • getUserOrders(userId) - Get user's orders
  • getStoreOrders(storeId) - Get store's orders
  • getOrderById(orderId) - Get order details
  • createOrder(userId, orderData) - Create new order
  • updateOrderStatus(orderId, status) - Update order status
  • cancelOrder(orderId, userId) - Cancel order

Features:

  • Multi-store order routing
  • Inventory reservation
  • Tax calculation via Stripe
  • Shipping rate calculation via EasyPost
  • Order status notifications
  • Trade credit application

Wishlist Service (wishlist.service.ts)

User wishlist management.

Key functions:

  • getUserWishlists(userId) - Get all user wishlists
  • createWishlist(userId, data, tier) - Create new wishlist
  • addItem(wishlistId, userId, isbn, bookData, priority) - Add book to wishlist
  • updateItem(itemId, userId, data) - Update wishlist item
  • removeItem(itemId, userId) - Remove from wishlist

Features:

  • Tier-based limits (free: 3 wishlists, premium: unlimited)
  • Priority levels (high, normal, low)
  • Status tracking (wish, reading, finished)
  • Public/private wishlists

Notification Service (notification.service.ts)

Notification creation and delivery.

Key functions:

  • createNotification(data) - Create notification record
  • getUserNotifications(userId, options) - Get user notifications
  • markAsRead(notificationId, userId) - Mark notification as read
  • markAllAsRead(userId) - Mark all as read

Features:

  • Push notification queueing (via Bull)
  • Email notification queueing
  • Notification types: order updates, followers, likes, stock alerts, etc.
  • Pagination support

Email Service (email.service.ts)

Transactional email sending.

Key functions:

  • sendEmail(params) - Send email via AWS SES
  • sendOrderConfirmation(order) - Order confirmation email
  • sendOrderStatusUpdate(order, status) - Order status change email
  • sendWelcomeEmail(user) - Welcome email

Features:

  • AWS SES integration
  • HTML and text email templates
  • Email queueing for reliability

Push Service (push.service.ts)

Push notification delivery.

Key functions:

  • sendPush(token, notification) - Send push notification
  • registerToken(userId, token, platform) - Register device token
  • removeToken(userId, token) - Remove device token

Features:

  • Firebase Cloud Messaging (FCM) integration
  • Multi-device support
  • Token management

Service Conventions

Error Handling

Services throw structured errors that controllers can handle:

// Not found
throw {
status: 404,
error: 'NotFound',
message: 'Resource not found'
};

// Forbidden
throw {
status: 403,
error: 'Forbidden',
message: 'Access denied'
};

// Bad request
throw {
status: 400,
error: 'BadRequest',
message: 'Invalid data'
};

// Upgrade required
throw {
status: 403,
error: 'upgrade_required',
message: 'Premium feature',
minTier: 'premium'
};

Ownership Checks

Services verify ownership before modifying resources:

export async function update(id: string, userId: string, data: UpdateData) {
const existing = await prisma.resource.findUnique({
where: { id },
});

if (!existing) {
throw { status: 404, error: 'NotFound', message: 'Not found' };
}

if (existing.userId !== userId) {
throw { status: 403, error: 'Forbidden', message: 'Access denied' };
}

// Proceed with update
}

Transactions

Use Prisma transactions for multi-step operations:

export async function createOrder(userId: string, orderData: OrderData) {
return await prisma.$transaction(async (tx) => {
// Create order
const order = await tx.order.create({
data: { /* ... */ },
});

// Reserve inventory
for (const item of orderData.items) {
await tx.inventory.update({
where: { id: item.inventoryId },
data: {
reservedQuantity: { increment: item.quantity },
},
});
}

// Deduct trade credit
if (orderData.tradeCreditCents > 0) {
await tx.tradeCreditAccount.update({
where: {
userId_storeId: { userId, storeId: order.storeId },
},
data: {
balanceCents: { decrement: orderData.tradeCreditCents },
},
});
}

return order;
});
}

Logging

Services log important operations:

import { logger } from '../lib/logger';

export async function create(userId: string, data: CreateData) {
const resource = await prisma.resource.create({ /* ... */ });

logger.info('resource.created', {
resourceId: resource.id,
userId
});

return resource;
}

// Log errors
try {
// ...
} catch (error) {
logger.error('operation.failed', {
error: error instanceof Error ? error.message : String(error),
userId
});
throw error;
}

Pagination

Services implement cursor-based pagination:

export async function list(options: { cursor?: string; limit: number }) {
const limit = options.limit || 20;

const items = await prisma.resource.findMany({
take: limit + 1, // Fetch one extra to check if there's more
...(options.cursor && {
cursor: { id: options.cursor },
skip: 1, // Skip the cursor item
}),
orderBy: { createdAt: 'desc' },
});

const hasMore = items.length > limit;
const data = hasMore ? items.slice(0, limit) : items;
const nextCursor = hasMore ? data[data.length - 1].id : null;

return {
data,
nextCursor,
hasMore,
};
}

Background Jobs

Services queue background jobs:

import { queueNotification } from '../jobs/notification.job';

export async function createNotification(data: NotificationData) {
// Create notification record
const notification = await prisma.notification.create({
data,
});

// Queue for delivery
await queueNotification({
notificationId: notification.id,
userId: data.userId,
type: data.type,
title: data.title,
body: data.body,
});

return notification;
}

Service Organization

Services are organized by domain:

src/services/
├── auth.service.ts # Authentication
├── users.service.ts # User management
├── user-preferences.service.ts # User preferences
├── book.service.ts # Book catalog
├── wishlist.service.ts # Wishlists
├── store.service.ts # Store management
├── inventory.service.ts # Store inventory
├── order.service.ts # Order management
├── cart.service.ts # Shopping cart
├── pos.service.ts # Point of sale
├── trade-credit.service.ts # Trade credit
├── trade-in.service.ts # Trade-ins
├── line.service.ts # Social lines
├── review.service.ts # Book reviews
├── note.service.ts # Personal notes
├── follow.service.ts # Social following
├── feed.service.ts # Activity feed
├── search.service.ts # Search
├── club.service.ts # Book clubs
├── challenge.service.ts # Reading challenges
├── moderation.service.ts # Content moderation
├── notification.service.ts # Notifications
├── email.service.ts # Email delivery
├── push.service.ts # Push notifications
├── address.service.ts # User addresses
├── stock-alert.service.ts # Stock alerts
├── shipping.service.ts # Shipping rates
├── tax.service.ts # Tax calculation
├── economics.service.ts # Platform economics
├── order-routing.service.ts # Order routing logic
├── home-store-pool.service.ts # Home store pool
└── storage.service.ts # File storage

Best Practices

  1. Single Responsibility - Each service handles one domain
  2. No HTTP Logic - Services don't know about HTTP requests/responses
  3. Throw Errors - Use structured error objects for controllers to handle
  4. Use Transactions - Wrap multi-step operations in Prisma transactions
  5. Log Operations - Log important events for debugging and monitoring
  6. Validate Business Rules - Enforce constraints in service layer
  7. Queue Background Work - Use Bull for async operations
  8. Cache When Appropriate - Use Redis for frequently accessed data
  9. Handle Race Conditions - Use database locks or optimistic concurrency
  10. Test Services - Write unit tests with mocked Prisma client