API Testing & Automation
REST API test automation with Axios and Cucumber — schema validation, chained requests, and BDD-driven scenarios.
How I use it
I build API test suites that go beyond basic status code checks. My approach combines Cucumber/Gherkin for readable scenarios with TypeScript services that handle authentication, request chaining, and response validation. At Fourvenues, this covers booking flows, payment processing, and event management endpoints.
Real example: Booking zone creation
This is based on a real test scenario from the Fourvenues platform. The Gherkin scenario describes the business flow, while the step definitions call a typed service layer.
Feature: Booking Zone Management
@api @booking-zones
Scenario: Create a booking zone with valid data
Given the professional is authenticated
And an active event exists for today
When I create a booking zone with:
| name | VIP Lounge |
| capacity | 50 |
| minSpend | 200 |
Then the response status should be 200
And the zone should appear in the event's zone list
@api @booking-zones
Scenario: Create a booking in the zone
Given the professional is authenticated
And a booking zone "VIP Lounge" exists
When I create a booking with:
| clientName | Test Client |
| pax | 4 |
| zone | VIP Lounge |
Then the response status should be 200
And the booking status should be "confirmed" Service layer pattern
Each API domain gets a dedicated service class. The service handles endpoint URLs, request construction, and response typing. Step definitions stay clean — they only orchestrate calls and assertions.
import { AxiosInstance } from 'axios';
import { BookingZone, Booking, BookingStatus } from '../types';
export enum BookingEndpoints {
ZONES = '/reservas_zonas',
BOOKINGS = '/reservados',
STATUS = '/reservados_estado',
MAP = '/listado_reservados_mapa',
}
export class BookingService {
constructor(private api: AxiosInstance) {}
async createZone(eventId: string, zone: Partial<BookingZone>) {
const response = await this.api.post(BookingEndpoints.ZONES, {
eventId,
...zone,
});
return response.data;
}
async createBooking(booking: Partial<Booking>) {
const response = await this.api.post(
BookingEndpoints.BOOKINGS,
booking
);
return response.data;
}
async updateStatus(bookingId: string, status: BookingStatus) {
const response = await this.api.put(
`${BookingEndpoints.STATUS}`,
{ id: bookingId, status }
);
return response.data;
}
} Step definitions
import { When, Then } from '@cucumber/cucumber';
import { strict as assert } from 'node:assert';
import { FvContext } from '../support/fvContext';
When('I create a booking zone with:', async function (
this: FvContext,
dataTable
) {
const data = dataTable.rowsHash();
this.response = await this.bookingService.createZone(
this.currentEventId,
{
name: data.name,
capacity: Number(data.capacity),
minSpend: Number(data.minSpend),
}
);
this.currentZoneId = this.response.id;
});
Then('the zone should appear in the event\'s zone list',
async function (this: FvContext) {
const zones = await this.bookingService.getZones(
this.currentEventId
);
const found = zones.find(z => z.id === this.currentZoneId);
assert.ok(found,
`Zone ${this.currentZoneId} not found in event zones`
);
}
); What I focus on
- Typed services — every endpoint wrapped in a typed class, endpoints as enums
- Shared context — Cucumber world object (
FvContext) carries state between steps - Custom assertions —
assert.strictEqual()with descriptive error messages overexpect().toBe() - Chained flows — create → book → pay → verify, each step reusing previous response data
- Environment-aware config — base URLs, auth tokens, and test data configurable per environment