Migrating to Nest.js and React
- 21 Apr 2024 |
- 02 Mins read
At Skillshare, we migrated our payments backend to Nest.js and our frontend to React. The migration improved developer experience, type safety, and maintainability.
Why Migrate?
Our legacy stack had issues:
- Tight coupling between components
- Limited type safety
- Difficult to test
- Poor developer experience
- Hard to scale
Backend: Nest.js Migration
Nest.js provides:
- TypeScript-first architecture
- Dependency injection
- Modular structure
- Built-in testing support
- Excellent documentation
Service Structure
// payment.service.ts
@Injectable()
export class PaymentService {
constructor(
private paymentRepository: PaymentRepository,
private notificationService: NotificationService,
) {}
async processPayment(dto: CreatePaymentDto): Promise<Payment> {
const payment = await this.paymentRepository.create(dto);
await this.notificationService.sendConfirmation(payment);
return payment;
}
}
// payment.controller.ts
@Controller('payments')
export class PaymentController {
constructor(private paymentService: PaymentService) {}
@Post()
async create(@Body() dto: CreatePaymentDto): Promise<Payment> {
return this.paymentService.processPayment(dto);
}
}
Module Organization
@Module({
imports: [TypeOrmModule.forFeature([Payment])],
controllers: [PaymentController],
providers: [PaymentService, PaymentRepository],
exports: [PaymentService],
})
export class PaymentModule {}
Frontend: React Migration
React provides:
- Component-based architecture
- Strong ecosystem
- TypeScript support
- Great developer tools
- Performance optimizations
Component Structure
interface PaymentFormProps {
onSubmit: (payment: PaymentData) => void;
}
function PaymentForm({ onSubmit }: PaymentFormProps) {
const [paymentData, setPaymentData] = useState<PaymentData>({
amount: 0,
currency: 'USD',
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(paymentData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="number"
value={paymentData.amount}
onChange={(e) => setPaymentData({
...paymentData,
amount: parseFloat(e.target.value),
})}
/>
<button type="submit">Pay</button>
</form>
);
}
Migration Strategy
- Parallel Run: Run old and new systems side-by-side
- Feature Flags: Gradually enable new system
- Incremental Migration: Move features one at a time
- Testing: Comprehensive testing at each step
- Rollback Plan: Ability to revert if needed
Testing
// Backend tests
describe('PaymentService', () => {
let service: PaymentService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [PaymentService, PaymentRepository],
}).compile();
service = module.get<PaymentService>(PaymentService);
});
it('should process payment', async () => {
const result = await service.processPayment({
amount: 100,
currency: 'USD',
});
expect(result).toBeDefined();
});
});
// Frontend tests
describe('PaymentForm', () => {
it('should submit payment data', () => {
const onSubmit = jest.fn();
render(<PaymentForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '100' },
});
fireEvent.click(screen.getByText('Pay'));
expect(onSubmit).toHaveBeenCalledWith({
amount: 100,
currency: 'USD',
});
});
});
Results
- Better type safety
- Improved developer experience
- Easier testing
- More maintainable code
- Faster feature development
"Modern frameworks improve productivity and code quality."
Lessons Learned
- Plan migration carefully
- Test thoroughly
- Migrate incrementally
- Train team on new stack
- Monitor performance