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 accountsignup(email, password, displayName)- User registrationlogin(email, password)- User loginrefreshTokens(refreshToken)- Refresh JWT tokensmigrateGuest(guestUserId, email, password, displayName)- Convert guest to full accountlogout(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/ISBNgetBookById(id)- Get book detailsgetBookByISBN(isbn)- Get or create book by ISBNcheckAvailability(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 inventoryaddInventory(storeId, data)- Add inventory itemupdateInventory(id, data)- Update inventorydeleteInventory(id)- Remove inventorysyncFromSquare(storeId)- Sync inventory from Square POSreserveInventory(inventoryId, quantity)- Reserve for orderreleaseReservation(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 ordersgetStoreOrders(storeId)- Get store's ordersgetOrderById(orderId)- Get order detailscreateOrder(userId, orderData)- Create new orderupdateOrderStatus(orderId, status)- Update order statuscancelOrder(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 wishlistscreateWishlist(userId, data, tier)- Create new wishlistaddItem(wishlistId, userId, isbn, bookData, priority)- Add book to wishlistupdateItem(itemId, userId, data)- Update wishlist itemremoveItem(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 recordgetUserNotifications(userId, options)- Get user notificationsmarkAsRead(notificationId, userId)- Mark notification as readmarkAllAsRead(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 SESsendOrderConfirmation(order)- Order confirmation emailsendOrderStatusUpdate(order, status)- Order status change emailsendWelcomeEmail(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 notificationregisterToken(userId, token, platform)- Register device tokenremoveToken(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
- Single Responsibility - Each service handles one domain
- No HTTP Logic - Services don't know about HTTP requests/responses
- Throw Errors - Use structured error objects for controllers to handle
- Use Transactions - Wrap multi-step operations in Prisma transactions
- Log Operations - Log important events for debugging and monitoring
- Validate Business Rules - Enforce constraints in service layer
- Queue Background Work - Use Bull for async operations
- Cache When Appropriate - Use Redis for frequently accessed data
- Handle Race Conditions - Use database locks or optimistic concurrency
- Test Services - Write unit tests with mocked Prisma client