Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Comprehensive RESTful API testing patterns covering HTTP methods, status codes, request/response validation, authentication, error handling, and contract testing.
.claude/skills/pramoddutta-api-testing-rest/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 411% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 319% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 312% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 288% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 405% | 0% |
You are an expert QA engineer specializing in REST API testing. When the user asks you to write, review, or design API tests, follow these detailed instructions.
GET - Retrieve resource(s), safe and idempotent
POST - Create new resource, not idempotent
PUT - Replace entire resource, idempotent
PATCH - Partial update, idempotent
DELETE - Remove resource, idempotent
HEAD - Same as GET but no response body
OPTIONS - Get supported methods for resourceSuccess (2xx):
200 OK - Successful GET, PUT, PATCH, DELETE
201 Created - Successful POST, resource created
204 No Content - Successful DELETE (no body returned)
Client Error (4xx):
400 Bad Request - Invalid request body or parameters
401 Unauthorized - Missing or invalid authentication
403 Forbidden - Authenticated but not authorized
404 Not Found - Resource doesn't exist
409 Conflict - Resource conflict (duplicate email)
422 Unprocessable - Validation error
Server Error (5xx):
500 Internal Error - Server error
503 Service Unavailable - Service down or overloadedtypescript// api-client.ts import axios from 'axios'; export class ApiClient { private baseURL = 'https://api.example.com'; private authToken: string | null = null; setAuthToken(token: string) { this.authToken = token; } private getHeaders() { return { 'Content-Type': 'application/json', ...(this.authToken && { Authorization: `Bearer ${this.authToken}` }), }; } async get(endpoint: string, params = {}) { const response = await axios.get(`${this.baseURL}${endpoint}`, { headers: this.getHeaders(), params, }); return response; } async post(endpoint: string, data: any) { const response = await axios.post(`${this.baseURL}${endpoint}`, data, { headers: this.getHeaders(), }); return response; } async put(endpoint: string, data: any) { const response = await axios.put(`${this.baseURL}${endpoint}`, data, { headers: this.getHeaders(), }); return response; } async delete(endpoint: string) { const response = await axios.delete(`${this.baseURL}${endpoint}`, { headers: this.getHeaders(), }); return response; } }
typescript// users.api.test.ts import { describe, it, expect, beforeAll } from 'vitest'; import { ApiClient } from './api-client'; describe('Users API', () => { const api = new ApiClient(); let createdUserId: string; beforeAll(async () => { // Authenticate before running tests const authResponse = await api.post('/auth/login', { email: 'test@example.com', password: 'password123', }); api.setAuthToken(authResponse.data.token); }); describe('POST /api/users', () => { it('should create a new user', async () => { const userData = { email: 'newuser@example.com', name: 'New User', role: 'user', }; const response = await api.post('/api/users', userData); // Assert status code expect(response.status).toBe(201); // Assert response structure expect(response.data).toHaveProperty('id'); expect(response.data).toHaveProperty('email', userData.email); expect(response.data).toHaveProperty('name', userData.name); expect(response.data).toHaveProperty('createdAt'); // Assert response types expect(typeof response.data.id).toBe('string'); expect(response.data.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); // Save for cleanup createdUserId = response.data.id; }); it('should return 400 for invalid email', async () => { try { await api.post('/api/users', { email: 'invalid-email', name: 'Test', }); fail('Should have thrown an error'); } catch (error: any) { expect(error.response.status).toBe(400); expect(error.response.data).toHaveProperty('error'); expect(error.response.data.error).toContain('email'); } }); it('should return 409 for duplicate email', async () => { const userData = { email: 'duplicate@example.com', name: 'Duplicate User', }; // Create first user await api.post('/api/users', userData); // Attempt to create duplicate try { await api.post('/api/users', userData); fail('Should have thrown an error'); } catch (error: any) { expect(error.response.status).toBe(409); expect(error.response.data.error).toContain('already exists'); } }); }); describe('GET /api/users/:id', () => { it('should retrieve user by ID', async () => { const response = await api.get(`/api/users/${createdUserId}`); expect(response.status).toBe(200); expect(response.data.id).toBe(createdUserId); expect(response.data).toHaveProperty('email'); expect(response.data).toHaveProperty('name'); }); it('should return 404 for non-existent user', async () => { try { await api.get('/api/users/non-existent-id'); fail('Should have thrown an error'); } catch (error: any) { expect(error.response.status).toBe(404); } }); }); describe('GET /api/users', () => { it('should list all users', async () => { const response = await api.get('/api/users'); expect(response.status).toBe(200); expect(Array.isArray(response.data)).toBe(true); expect(response.data.length).toBeGreaterThan(0); // Validate structure of first user const firstUser = response.data[0]; expect(firstUser).toHaveProperty('id'); expect(firstUser).toHaveProperty('email'); expect(firstUser).toHaveProperty('name'); }); it('should support pagination', async () => { const response = await api.get('/api/users', { page: 1, limit: 10, }); expect(response.status).toBe(200); expect(response.data).toHaveProperty('items'); expect(response.data).toHaveProperty('total'); expect(response.data).toHaveProperty('page', 1); expect(response.data).toHaveProperty('limit', 10); expect(response.data.items.length).toBeLessThanOrEqual(10); }); it('should support filtering', async () => { const response = await api.get('/api/users', { role: 'admin', }); expect(response.status).toBe(200); expect(Array.isArray(response.data)).toBe(true); // All users should be admins response.data.forEach((user: any) => { expect(user.role).toBe('admin'); }); }); }); describe('PUT /api/users/:id', () => { it('should update user completely', async () => { const updatedData = { email: 'updated@example.com', name: 'Updated Name', role: 'admin', }; const response = await api.put(`/api/users/${createdUserId}`, updatedData); expect(response.status).toBe(200); expect(response.data.email).toBe(updatedData.email); expect(response.data.name).toBe(updatedData.name); expect(response.data.role).toBe(updatedData.role); }); it('should return 404 for non-existent user', async () => { try { await api.put('/api/users/non-existent', { name: 'Test' }); fail('Should have thrown an error'); } catch (error: any) { expect(error.response.status).toBe(404); } }); }); describe('DELETE /api/users/:id', () => { it('should delete user', async () => { const response = await api.delete(`/api/users/${createdUserId}`); expect(response.status).toBe(204); // Verify deletion try { await api.get(`/api/users/${createdUserId}`); fail('User should be deleted'); } catch (error: any) { expect(error.response.status).toBe(404); } }); it('should return 404 when deleting non-existent user', async () => { try { await api.delete('/api/users/non-existent'); fail('Should have thrown an error'); } catch (error: any) { expect(error.response.status).toBe(404); } }); }); });
python# api_client.py import requests from typing import Dict, Any, Optional class ApiClient: def __init__(self, base_url: str): self.base_url = base_url self.session = requests.Session() self.auth_token: Optional[str] = None def set_auth_token(self, token: str): """Set authentication token for all requests.""" self.auth_token = token self.session.headers.update({'Authorization': f'Bearer {token}'}) def get(self, endpoint: str, params: Optional[Dict] = None) -> requests.Response: """Perform GET request.""" url = f"{self.base_url}{endpoint}" return self.session.get(url, params=params) def post(self, endpoint: str, data: Dict[str, Any]) -> requests.Response: """Perform POST request.""" url = f"{self.base_url}{endpoint}" return self.session.post(url, json=data) def put(self, endpoint: str, data: Dict[str, Any]) -> requests.Response: """Perform PUT request.""" url = f"{self.base_url}{endpoint}" return self.session.put(url, json=data) def delete(self, endpoint: str) -> requests.Response: """Perform DELETE request.""" url = f"{self.base_url}{endpoint}" return self.session.delete(url)
python# test_users_api.py import pytest from api_client import ApiClient @pytest.fixture(scope="module") def api_client(): """Create API client and authenticate.""" client = ApiClient("https://api.example.com") # Authenticate response = client.post("/auth/login", { "email": "test@example.com", "password": "password123" }) assert response.status_code == 200 client.set_auth_token(response.json()["token"]) return client @pytest.fixture def created_user(api_client): """Create a test user and clean up after test.""" response = api_client.post("/api/users", { "email": "testuser@example.com", "name": "Test User", }) user_id = response.json()["id"] yield user_id # Cleanup api_client.delete(f"/api/users/{user_id}") class TestUsersAPI: """Test suite for Users API.""" def test_create_user_success(self, api_client): """Should create a new user with valid data.""" # Arrange user_data = { "email": "newuser@example.com", "name": "New User", "role": "user", } # Act response = api_client.post("/api/users", user_data) # Assert assert response.status_code == 201 data = response.json() assert "id" in data assert data["email"] == user_data["email"] assert data["name"] == user_data["name"] assert "createdAt" in data # Cleanup api_client.delete(f"/api/users/{data['id']}") def test_create_user_invalid_email(self, api_client): """Should return 400 for invalid email.""" response = api_client.post("/api/users", { "email": "invalid-email", "name": "Test User", }) assert response.status_code == 400 assert "error" in response.json() def test_get_user_by_id(self, api_client, created_user): """Should retrieve user by ID.""" response = api_client.get(f"/api/users/{created_user}") assert response.status_code == 200 data = response.json() assert data["id"] == created_user assert "email" in data assert "name" in data def test_get_user_not_found(self, api_client): """Should return 404 for non-existent user.""" response = api_client.get("/api/users/non-existent-id") assert response.status_code == 404 def test_list_users(self, api_client): """Should list all users.""" response = api_client.get("/api/users") assert response.status_code == 200 data = response.json() assert isinstance(data, list) assert len(data) > 0 assert "id" in data[0] assert "email" in data[0] def test_update_user(self, api_client, created_user): """Should update user data.""" updated_data = { "email": "updated@example.com", "name": "Updated Name", } response = api_client.put(f"/api/users/{created_user}", updated_data) assert response.status_code == 200 data = response.json() assert data["email"] == updated_data["email"] assert data["name"] == updated_data["name"] def test_delete_user(self, api_client, created_user): """Should delete user.""" response = api_client.delete(f"/api/users/{created_user}") assert response.status_code == 204 # Verify deletion get_response = api_client.get(f"/api/users/{created_user}") assert get_response.status_code == 404
java// UserApiTest.java import io.restassured.RestAssured; import io.restassured.response.Response; import org.junit.jupiter.api.*; import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class UserApiTest { private static String authToken; private static String createdUserId; @BeforeAll public static void setup() { RestAssured.baseURI = "https://api.example.com"; // Authenticate Response authResponse = given() .contentType("application/json") .body("{ \"email\": \"test@example.com\", \"password\": \"password123\" }") .when() .post("/auth/login") .then() .statusCode(200) .extract().response(); authToken = authResponse.jsonPath().getString("token"); } @Test @Order(1) public void testCreateUser() { String requestBody = """ { "email": "newuser@example.com", "name": "New User", "role": "user" } """; Response response = given() .header("Authorization", "Bearer " + authToken) .contentType("application/json") .body(requestBody) .when() .post("/api/users") .then() .statusCode(201) .body("id", notNullValue()) .body("email", equalTo("newuser@example.com")) .body("name", equalTo("New User")) .body("createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T.*")) .extract().response(); createdUserId = response.jsonPath().getString("id"); } @Test @Order(2) public void testGetUser() { given() .header("Authorization", "Bearer " + authToken) .when() .get("/api/users/" + createdUserId) .then() .statusCode(200) .body("id", equalTo(createdUserId)) .body("email", notNullValue()) .body("name", notNullValue()); } @Test @Order(3) public void testUpdateUser() { String updateBody = """ { "email": "updated@example.com", "name": "Updated Name" } """; given() .header("Authorization", "Bearer " + authToken) .contentType("application/json") .body(updateBody) .when() .put("/api/users/" + createdUserId) .then() .statusCode(200) .body("email", equalTo("updated@example.com")) .body("name", equalTo("Updated Name")); } @Test @Order(4) public void testDeleteUser() { given() .header("Authorization", "Bearer " + authToken) .when() .delete("/api/users/" + createdUserId) .then() .statusCode(204); // Verify deletion given() .header("Authorization", "Bearer " + authToken) .when() .get("/api/users/" + createdUserId) .then() .statusCode(404); } }
typescriptimport Ajv from 'ajv'; const userSchema = { type: 'object', required: ['id', 'email', 'name', 'createdAt'], properties: { id: { type: 'string', pattern: '^[a-zA-Z0-9-]+$' }, email: { type: 'string', format: 'email' }, name: { type: 'string', minLength: 1 }, role: { type: 'string', enum: ['user', 'admin', 'moderator'] }, createdAt: { type: 'string', format: 'date-time' }, }, additionalProperties: false, }; test('should match user schema', async () => { const response = await api.get('/api/users/123'); const ajv = new Ajv(); const validate = ajv.compile(userSchema); const valid = validate(response.data); expect(valid).toBe(true); if (!valid) { console.error(validate.errors); } });
REST API testing ensures your backend contract is solid and reliable. Test thoroughly, validate rigorously.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 21,819 | 19,947 | -9% | 1 | 1 | 0% | 2,751 | 7,736 | +181% | 0 | 0 | — |
case-02 | pass→pass | 15,782 | 20,143 | +28% | 1 | 1 | 0% | 1,888 | 7,920 | +319% | 0 | 0 | — |
case-03 | pass→pass | 9,428 | 26,273 | +179% | 1 | 1 | 0% | 1,697 | 6,995 | +312% | 0 | 0 | — |
case-04 | pass→pass | 18,485 | 21,162 | +14% | 1 | 1 | 0% | 2,079 | 8,071 | +288% | 0 | 0 | — |
case-05 | fail→fail | 19,213 | 22,603 | +18% | 1 | 1 | 0% | 2,320 | 8,346 | +260% | 0 | 0 | — |
case-06 | pass→pass | 8,926 | 18,826 | +111% | 1 | 1 | 0% | 1,492 | 7,534 | +405% | 0 | 0 | — |
case-07 | fail→pass | 15,166 | 14,552 | -4% | 1 | 1 | 0% | 1,523 | 7,776 | +411% | 0 | 0 | — |
case-08 | pass→pass | 15,943 | 17,570 | +10% | 1 | 1 | 0% | 2,837 | 8,462 | +198% | 0 | 0 | — |
case-09 | pass→pass | 17,466 | 11,453 | -34% | 1 | 1 | 0% | 1,868 | 7,267 | +289% | 0 | 0 | — |
case-10 | pass→pass | 14,887 | 23,351 | +57% | 1 | 1 | 0% | 2,646 | 8,327 | +215% | 0 | 0 | — |
case-11 | pass→pass | 19,702 | 20,753 | +5% | 1 | 1 | 0% | 2,481 | 8,177 | +230% | 0 | 0 | — |
case-12 | pass→pass | 8,167 | 9,052 | +11% | 1 | 1 | 0% | 1,334 | 6,962 | +422% | 0 | 0 | — |
case-13 | pass→pass | 12,392 | 13,286 | +7% | 1 | 1 | 0% | 1,973 | 7,562 | +283% | 0 | 0 | — |
case-14 | pass→pass | 10,932 | 16,845 | +54% | 1 | 1 | 0% | 1,842 | 7,365 | +300% | 0 | 0 | — |
case-15 | pass→pass | 12,064 | 15,835 | +31% | 1 | 1 | 0% | 1,032 | 7,412 | +618% | 0 | 0 | — |
case-16 | pass→pass | 15,429 | 17,324 | +12% | 1 | 1 | 0% | 2,998 | 8,474 | +183% | 0 | 0 | — |
case-17 | pass→pass | 20,183 | 23,494 | +16% | 1 | 1 | 0% | 3,700 | 8,413 | +127% | 0 | 0 | — |
case-18 | pass→pass | 11,736 | 14,200 | +21% | 1 | 1 | 0% | 1,694 | 7,505 | +343% | 0 | 0 | — |
case-19 | pass→pass | 22,728 | 14,756 | -35% | 1 | 1 | 0% | 2,870 | 8,208 | +186% | 0 | 0 | — |
case-20 | pass→pass | 12,342 | 19,203 | +56% | 1 | 1 | 0% | 2,265 | 7,922 | +250% | 0 | 0 | — |
case-21 | pass→pass | 15,188 | 13,941 | -8% | 1 | 1 | 0% | 1,946 | 6,957 | +258% | 0 | 0 | — |
case-22 | pass→pass | 11,660 | 13,103 | +12% | 1 | 1 | 0% | 1,095 | 6,539 | +497% | 0 | 0 | — |
case-23 | pass→pass | 25,436 | 23,421 | -8% | 1 | 1 | 0% | 3,201 | 8,611 | +169% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.