Recipes
Common use cases and production patterns.
1. Payment Processing
typescript
import { Injectable, Controller, Post, Body, Res } from '@nestjs/common';
import { Idempotent } from '@nestjs-redisx/idempotency';
import { CreatePaymentDto, PaymentGateway, PaymentService, EmailService } from '../types';
@Injectable()
@Controller()
export class PaymentController {
constructor(
private readonly paymentGateway: PaymentGateway,
private readonly paymentService: PaymentService,
private readonly emailService: EmailService,
) {}
@Post('payments')
@Idempotent({
ttl: 86400, // 24 hours
cacheHeaders: ['X-Payment-Id', 'X-Transaction-Reference'],
})
async createPayment(
@Body() dto: CreatePaymentDto,
@Res() res: any,
) {
// Charge payment gateway
const payment = await this.paymentGateway.charge({
amount: dto.amount,
currency: dto.currency,
source: dto.source,
});
// Record in database
await this.paymentService.record(payment);
// Send receipt
await this.emailService.sendReceipt(payment);
// Set headers for client
res.setHeader('X-Payment-Id', payment.id);
res.setHeader('X-Transaction-Reference', payment.transactionRef);
return res.status(201).json({
id: payment.id,
status: payment.status,
amount: payment.amount,
currency: payment.currency,
});
}
}2. Order Creation with Side Effects
typescript
import { Injectable, Controller, Post, Body, Res } from '@nestjs/common';
import { Idempotent } from '@nestjs-redisx/idempotency';
import { CreateOrderDto, OrderService, InventoryService, EmailService, WarehouseService, AnalyticsService, User } from '../types';
@Injectable()
@Controller()
export class OrderController {
constructor(
private readonly orderService: OrderService,
private readonly inventoryService: InventoryService,
private readonly emailService: EmailService,
private readonly warehouseService: WarehouseService,
private readonly analyticsService: AnalyticsService,
) {}
@Post('orders')
@Idempotent({
ttl: 3600, // 1 hour
cacheHeaders: ['Location', 'X-Order-Number'],
})
async createOrder(
@Body() dto: CreateOrderDto,
@Res() res: any,
) {
// All of these execute ONCE per idempotency key
const order = await this.orderService.create({
items: dto.items,
});
// Reserve inventory
await this.inventoryService.reserve(order.items);
// Send confirmation email
await this.emailService.sendOrderConfirmation(order);
// Notify warehouse
await this.warehouseService.notifyNewOrder(order);
// Track analytics
await this.analyticsService.trackOrderCreated(order);
res.setHeader('Location', `/orders/${order.id}`);
res.setHeader('X-Order-Number', order.orderNumber);
return res.status(201).json(order);
}
}3. Webhook Handler
typescript
import { Injectable, Controller, Post, Body, Headers } from '@nestjs/common';
import { Idempotent } from '@nestjs-redisx/idempotency';
import { StripeWebhookEvent, StripeService } from '../types';
@Injectable()
@Controller()
export class WebhookController {
constructor(
private readonly stripeService: StripeService,
) {}
@Post('webhooks/stripe')
@Idempotent({
ttl: 86400,
keyExtractor: (ctx) => {
const req = ctx.switchToHttp().getRequest();
// Use Stripe's webhook ID
return req.headers['stripe-webhook-id'];
},
// Body may vary slightly, don't validate fingerprint
fingerprintFields: ['method', 'path'],
})
async handleStripeWebhook(
@Body() event: StripeWebhookEvent,
@Headers('stripe-signature') signature: string,
) {
// Verify signature
this.stripeService.verifySignature(event, signature);
// Process event (only once even if Stripe retries)
switch (event.type) {
case 'payment_intent.succeeded':
await this.handlePaymentSuccess(event.data);
break;
case 'payment_intent.failed':
await this.handlePaymentFailure(event.data);
break;
}
return { received: true };
}
private async handlePaymentSuccess(data: unknown): Promise<void> {}
private async handlePaymentFailure(data: unknown): Promise<void> {}
}4. Subscription Management
typescript
import { Injectable, Controller, Post, Delete, Body, Param, Res } from '@nestjs/common';
import { Idempotent } from '@nestjs-redisx/idempotency';
import { CreateSubscriptionDto, SubscriptionService, BillingService, EmailService, ScheduleService, User } from '../types';
@Injectable()
@Controller()
export class SubscriptionController {
constructor(
private readonly subscriptionService: SubscriptionService,
private readonly billingService: BillingService,
private readonly emailService: EmailService,
private readonly scheduleService: ScheduleService,
) {}
@Post('subscriptions')
@Idempotent({
ttl: 3600,
cacheHeaders: ['X-Subscription-Id'],
})
async createSubscription(
@Body() dto: CreateSubscriptionDto,
@Res() res: any,
) {
// Create subscription
const subscription = await this.subscriptionService.create({
planId: dto.planId,
paymentMethodId: dto.paymentMethodId,
});
// Charge first payment
await this.billingService.chargeInitial(subscription);
// Send welcome email
await this.emailService.sendWelcome({}, subscription);
// Set up recurring billing
await this.scheduleService.scheduleRecurring(subscription);
res.setHeader('X-Subscription-Id', subscription.id);
return res.status(201).json(subscription);
}
@Delete('subscriptions/:id')
@Idempotent({
ttl: 3600,
keyExtractor: (ctx) => {
const req = ctx.switchToHttp().getRequest();
return `cancel-subscription-${req.params.id}`;
},
})
async cancelSubscription(@Param('id') id: string) {
const subscription = await this.subscriptionService.findOne(id);
// Cancel at billing provider
await this.billingService.cancel(subscription);
// Update database
await this.subscriptionService.markCancelled(id);
// Send confirmation
await this.emailService.sendCancellationConfirmation(subscription);
return { status: 'cancelled' };
}
}5. Account Registration
typescript
import { Injectable, Controller, Post, Body, ConflictException } from '@nestjs/common';
import { createHash } from 'crypto';
import { Idempotent } from '@nestjs-redisx/idempotency';
import { RegisterDto, UserService, TokenService, EmailService, SettingsService, AnalyticsService } from '../types';
@Injectable()
@Controller()
export class AuthController {
constructor(
private readonly userService: UserService,
private readonly tokenService: TokenService,
private readonly emailService: EmailService,
private readonly settingsService: SettingsService,
private readonly analyticsService: AnalyticsService,
) {}
@Post('register')
@Idempotent({
ttl: 3600,
keyExtractor: (ctx) => {
const req = ctx.switchToHttp().getRequest();
// Use email as key (natural idempotency)
return `register-${req.body.email}`;
},
})
async register(@Body() dto: RegisterDto) {
// Check if email already exists
const existing = await this.userService.findByEmail(dto.email);
if (existing) {
throw new ConflictException('Email already registered');
}
// Create user
const user = await this.userService.create({
email: dto.email,
password: await this.hashPassword(dto.password),
name: dto.name,
});
// Send verification email
const token = await this.tokenService.generateVerification(user);
await this.emailService.sendVerification(user.email, token);
// Create default settings
await this.settingsService.createDefaults(user.id);
// Track signup
await this.analyticsService.trackSignup(user);
return {
id: user.id,
email: user.email,
message: 'Verification email sent',
};
}
private async hashPassword(password: string): Promise<string> {
return createHash('sha256').update(password).digest('hex');
}
}6. Money Transfer
typescript
import { Injectable, Controller, Post, Body, Res, ForbiddenException, BadRequestException } from '@nestjs/common';
import { Idempotent } from '@nestjs-redisx/idempotency';
import { CreateTransferDto, AccountService, TransferService, NotificationService, DatabaseTx, User } from '../types';
@Injectable()
@Controller()
export class TransferController {
constructor(
private readonly accountService: AccountService,
private readonly transferService: TransferService,
private readonly notificationService: NotificationService,
private readonly db: DatabaseTx,
) {}
@Post('transfers')
@Idempotent({
ttl: 86400,
cacheHeaders: ['X-Transfer-Id', 'X-Transaction-Hash'],
})
async createTransfer(
@Body() dto: CreateTransferDto,
@Res() res: any,
) {
// Validate accounts
const fromAccount = await this.accountService.findOne(dto.fromAccountId);
const toAccount = await this.accountService.findOne(dto.toAccountId);
// Check balance
if (fromAccount.balance < dto.amount) {
throw new BadRequestException('Insufficient funds');
}
// Execute transfer (atomic)
const transfer = await this.db.transaction(async (tx: any) => {
// Debit from account
await this.accountService.debit(
dto.fromAccountId,
dto.amount,
{ transaction: tx },
);
// Credit to account
await this.accountService.credit(
dto.toAccountId,
dto.amount,
{ transaction: tx },
);
// Record transfer
return await this.transferService.create(
{
fromAccountId: dto.fromAccountId,
toAccountId: dto.toAccountId,
amount: dto.amount,
currency: dto.currency,
},
{ transaction: tx },
);
});
// Send notifications
await Promise.all([
this.notificationService.notifyTransferSent(fromAccount, transfer),
this.notificationService.notifyTransferReceived(toAccount, transfer),
]);
res.setHeader('X-Transfer-Id', transfer.id);
res.setHeader('X-Transaction-Hash', transfer.hash);
return res.status(201).json(transfer);
}
}7. Batch Processing
typescript
import { Injectable, Controller, Post, Body } from '@nestjs/common';
import { Idempotent } from '@nestjs-redisx/idempotency';
import { ProcessBatchDto, BatchService, EmailService } from '../types';
@Injectable()
@Controller()
export class BatchController {
constructor(
private readonly batchService: BatchService,
private readonly emailService: EmailService,
) {}
@Post('batch/process')
@Idempotent({
ttl: 3600,
keyExtractor: (ctx) => {
const req = ctx.switchToHttp().getRequest();
return `batch-${req.body.batchId}`;
},
})
async processBatch(@Body() dto: ProcessBatchDto) {
const items = await this.batchService.getItems(dto.batchId);
const results = await Promise.allSettled(
items.map((item: any) => this.processItem(item)),
);
const summary = {
total: results.length,
succeeded: results.filter(r => r.status === 'fulfilled').length,
failed: results.filter(r => r.status === 'rejected').length,
};
// Mark batch as processed
await this.batchService.markCompleted(dto.batchId, summary);
// Send notification
await this.emailService.sendBatchReport(dto.batchId, summary);
return summary;
}
private async processItem(item: any): Promise<any> {
return item;
}
}8. Email Campaign Send
typescript
import { Injectable, Controller, Post, Param, ConflictException } from '@nestjs/common';
import { Idempotent } from '@nestjs-redisx/idempotency';
import { CampaignService, EmailQueue } from '../types';
@Injectable()
@Controller()
export class CampaignController {
constructor(
private readonly campaignService: CampaignService,
private readonly emailQueue: EmailQueue,
) {}
@Post('campaigns/:id/send')
@Idempotent({
ttl: 86400,
keyExtractor: (ctx) => {
const req = ctx.switchToHttp().getRequest();
return `campaign-send-${req.params.id}`;
},
})
async sendCampaign(@Param('id') campaignId: string) {
const campaign = await this.campaignService.findOne(campaignId);
if (campaign.status === 'sent') {
throw new ConflictException('Campaign already sent');
}
// Get recipients
const recipients = await this.campaignService.getRecipients(campaignId);
// Queue emails
await Promise.all(
recipients.map((recipient: any) =>
this.emailQueue.add('send-campaign-email', {
campaignId,
recipientId: recipient.id,
}),
),
);
// Mark as sent
await this.campaignService.markSent(campaignId);
return {
campaignId,
status: 'sent',
recipientCount: recipients.length,
};
}
}Next Steps
- Troubleshooting — Debug issues
- Overview — Back to overview