Accept Payments with Paystack in NestJS
To accept Paystack payments in NestJS, create a PaystackService that wraps the Paystack REST API calls. Inject it into a PaymentController that exposes endpoints for initialization and verification. Use ConfigService to manage your secret key. Initialize transactions server-side and redirect customers to Paystack checkout.
Project Setup
Create a new NestJS project or use an existing one:
npx @nestjs/cli new paystack-nestjs
cd paystack-nestjs
npm install @nestjs/config
Add your Paystack keys to a .env file:
PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
BASE_URL=http://localhost:3000
Register ConfigModule in your AppModule:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PaymentModule } from './payment/payment.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
PaymentModule,
],
})
export class AppModule {}
Create the Paystack Service
Create a service that wraps the Paystack API calls:
// src/payment/paystack.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class PaystackService {
private readonly secretKey: string;
private readonly baseUrl = 'https://api.paystack.co';
constructor(private configService: ConfigService) {
this.secretKey = this.configService.get('PAYSTACK_SECRET_KEY');
}
async initializeTransaction(email: string, amount: number, reference: string, callbackUrl: string) {
var response = await fetch(this.baseUrl + '/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.secretKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
amount: Math.round(amount * 100),
reference: reference,
callback_url: callbackUrl,
}),
});
return response.json();
}
async verifyTransaction(reference: string) {
var response = await fetch(
this.baseUrl + '/transaction/verify/' + encodeURIComponent(reference),
{
headers: {
Authorization: 'Bearer ' + this.secretKey,
},
}
);
return response.json();
}
}
Create the Payment Controller
// src/payment/payment.controller.ts
import { Controller, Post, Get, Body, Query, Res } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Response } from 'express';
import { PaystackService } from './paystack.service';
@Controller('api/payment')
export class PaymentController {
constructor(
private paystackService: PaystackService,
private configService: ConfigService,
) {}
@Post('initialize')
async initialize(@Body() body: { email: string; amount: number }) {
var reference = 'order_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
var callbackUrl = this.configService.get('BASE_URL') + '/api/payment/callback';
var data = await this.paystackService.initializeTransaction(
body.email,
body.amount,
reference,
callbackUrl,
);
if (!data.status) {
return { error: data.message };
}
// TODO: Save reference + amount to database
return {
authorization_url: data.data.authorization_url,
access_code: data.data.access_code,
reference: data.data.reference,
};
}
@Get('callback')
async callback(@Query('reference') reference: string, @Res() res: Response) {
if (!reference) {
return res.redirect('/payment/failed?error=missing_reference');
}
var data = await this.paystackService.verifyTransaction(reference);
if (data.status && data.data.status === 'success') {
// TODO: Verify amount matches, mark order as paid
return res.redirect('/payment/success?reference=' + reference);
}
return res.redirect('/payment/failed?reference=' + reference);
}
@Get('verify')
async verify(@Query('reference') reference: string) {
if (!reference) {
return { error: 'Missing reference' };
}
var data = await this.paystackService.verifyTransaction(reference);
if (data.status && data.data.status === 'success') {
return {
success: true,
amount: data.data.amount / 100,
currency: data.data.currency,
};
}
return { success: false };
}
}
Wire Up the Module
// src/payment/payment.module.ts
import { Module } from '@nestjs/common';
import { PaymentController } from './payment.controller';
import { PaystackService } from './paystack.service';
@Module({
controllers: [PaymentController],
providers: [PaystackService],
exports: [PaystackService],
})
export class PaymentModule {}
Exporting PaystackService lets other modules (like an OrderModule or SubscriptionModule) inject it and use it for their own payment needs.
Add DTO Validation
NestJS supports request validation with class-validator. Create a DTO for the payment initialization:
// src/payment/dto/initialize-payment.dto.ts
import { IsEmail, IsNumber, Min } from 'class-validator';
export class InitializePaymentDto {
@IsEmail()
email: string;
@IsNumber()
@Min(1)
amount: number;
}
Install the required packages and enable the global validation pipe:
npm install class-validator class-transformer
// src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
var app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.listen(3000);
}
bootstrap();
Now your endpoint automatically rejects requests with invalid emails or negative amounts before your code runs.
Security and Production Notes
- Use ConfigService. Never hardcode your Paystack secret key. ConfigService reads from environment variables and keeps secrets out of your codebase.
- Verify amounts. Look up the correct price from your database. Never trust what the client sends.
- Set up webhooks. The redirect callback can fail if the customer closes their browser. See Handle Paystack Webhooks in NestJS.
- Prevent double-granting. Both webhook and callback can fire for the same transaction. Use an idempotent fulfillment function.
- Enable CORS. If your frontend is on a different domain, configure CORS in main.ts.
Key Takeaways
- ✓NestJS services encapsulate the Paystack API calls. Inject the PaystackService into any controller that needs payment functionality.
- ✓Use ConfigService from @nestjs/config to access your Paystack secret key. Never hardcode secrets.
- ✓NestJS uses TypeScript by default, so you get type safety for Paystack API request and response objects.
- ✓Amounts are in kobo (NGN), pesewas (GHS), or cents (ZAR/KES/USD). Multiply the display price by 100.
- ✓Always verify the transaction server-side after the redirect. The redirect callback alone is not proof of payment.
- ✓No Paystack SDK is needed. Use the built-in fetch or HttpModule from @nestjs/axios for HTTP calls.
Frequently Asked Questions
- Should I use HttpModule or fetch for Paystack API calls in NestJS?
- Either works. The built-in fetch in Node 18+ is simpler and requires no extra packages. HttpModule from @nestjs/axios gives you interceptors and observables if you prefer the RxJS style. The Paystack API calls are identical either way.
- Can I use Guards to protect payment endpoints?
- Yes. Use AuthGuard to ensure only authenticated users can initialize payments. This also lets you pull the customer email from the authenticated user instead of trusting the request body.
- How do I test the PaystackService?
- Mock the fetch calls in your unit tests. Create a test module with ConfigService providing test values. For integration tests, use Paystack test keys and the test card numbers from the Paystack documentation.
- Does NestJS work with Paystack webhooks?
- Yes. Create a webhook controller that reads the raw body, verifies the HMAC signature, and processes events. NestJS supports raw body parsing through the rawBody option in NestFactory.create. See our dedicated webhook guide.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon