API Testing REST

Comprehensive RESTful API testing patterns covering HTTP methods, status codes, request/response validation, authentication, error handling, and contract testing.

97 stars

Best use case

API Testing REST is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Comprehensive RESTful API testing patterns covering HTTP methods, status codes, request/response validation, authentication, error handling, and contract testing.

Teams using API Testing REST should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/api-testing-rest/SKILL.md --create-dirs "https://raw.githubusercontent.com/PramodDutta/qaskills/main/seed-skills/api-testing-rest/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/api-testing-rest/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How API Testing REST Compares

Feature / AgentAPI Testing RESTStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Comprehensive RESTful API testing patterns covering HTTP methods, status codes, request/response validation, authentication, error handling, and contract testing.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# API Testing REST Skill

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.

## Core Principles

1. **Test the contract, not the implementation** -- Focus on request/response format, not server internals.
2. **Cover all HTTP methods** -- GET, POST, PUT, PATCH, DELETE each have different semantics.
3. **Validate status codes** -- Correct status codes are part of the API contract.
4. **Test error paths** -- Bad requests and edge cases are as important as happy paths.
5. **Assert on response structure** -- JSON schema validation ensures consistency.

## REST API Fundamentals

### HTTP Methods and Their Semantics

```
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 resource
```

### HTTP Status Codes

```
Success (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 overloaded
```

## Testing Patterns with Different Tools

### 1. JavaScript/TypeScript with Axios/Fetch

```typescript
// 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);
      }
    });
  });
});
```

### 2. Python with requests/pytest

```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
```

### 3. Java with REST Assured

```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);
    }
}
```

## JSON Schema Validation

```typescript
import 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);
  }
});
```

## Best Practices

1. **Test all CRUD operations** -- Create, Read, Update, Delete for each resource.
2. **Validate response schemas** -- Use JSON Schema validation.
3. **Test authentication/authorization** -- Verify protected endpoints.
4. **Test error responses** -- 4xx and 5xx scenarios are critical.
5. **Use fixtures for test data** -- Create and clean up test data.
6. **Test pagination and filtering** -- Verify query parameters work correctly.
7. **Assert on headers** -- Content-Type, Cache-Control, etc.
8. **Test idempotency** -- PUT/DELETE should be repeatable.
9. **Verify status codes** -- Correct codes are part of the contract.
10. **Clean up test data** -- Don't pollute the database.

## Anti-Patterns to Avoid

1. **Not testing error cases** -- Happy path alone is insufficient.
2. **Hardcoding IDs** -- Use dynamic test data.
3. **Not cleaning up** -- Test data should be removed after tests.
4. **Testing against production** -- Always use test/staging environments.
5. **Ignoring response times** -- Performance matters.
6. **Not validating response structure** -- Schema validation is essential.
7. **Sharing state between tests** -- Each test should be independent.
8. **Not testing edge cases** -- Empty lists, large payloads, special characters.
9. **Ignoring HTTP semantics** -- Use correct methods and status codes.
10. **Not documenting assumptions** -- Comment on expected API behavior.

REST API testing ensures your backend contract is solid and reliable. Test thoroughly, validate rigorously.

Related Skills

Zod Schema Testing

97
from PramodDutta/qaskills

Comprehensive testing patterns for Zod schemas covering validation testing, transform testing, error message verification, and integration with API endpoints and forms

YARA Rule Testing

97
from PramodDutta/qaskills

Writing and testing YARA rules for malware detection, threat hunting, and file classification with rule validation and false-positive rate testing.

xUnit.net Testing

97
from PramodDutta/qaskills

Comprehensive xUnit.net testing skill for writing reliable unit, integration, and acceptance tests in C# with [Fact], [Theory], fixtures, dependency injection, and parallel execution strategies.

XSS Testing Patterns

97
from PramodDutta/qaskills

Cross-site scripting vulnerability testing covering reflected, stored, and DOM-based XSS with sanitization validation and CSP bypass detection.

XCUITest iOS Testing

97
from PramodDutta/qaskills

iOS UI testing with XCUITest framework covering element queries, gesture simulation, accessibility testing, and Xcode test plan configuration.

Advanced WebSocket Testing

97
from PramodDutta/qaskills

WebSocket testing including connection lifecycle, reconnection logic, message ordering, backpressure handling, and binary frame testing.

Webhook Testing

97
from PramodDutta/qaskills

Testing webhook implementations including delivery verification, retry logic, signature validation, idempotency, and failure handling patterns.

Core Web Vitals Testing

97
from PramodDutta/qaskills

Testing and monitoring Core Web Vitals (LCP, FID, CLS, INP, TTFB) to ensure web performance meets Google search ranking thresholds.

WCAG Accessibility Testing

97
from PramodDutta/qaskills

Automated WCAG 2.2 AA/AAA compliance testing with axe-core, Pa11y, and manual testing patterns for keyboard navigation, screen readers, and color contrast.

WebAssembly Testing

97
from PramodDutta/qaskills

Testing WebAssembly modules including compilation verification, memory management, interop testing, and performance benchmarking of WASM components.

Vue Test Utils Testing

97
from PramodDutta/qaskills

Vue.js component testing using Vue Test Utils with mount/shallow mount, event simulation, Vuex/Pinia store testing, and composition API testing.

Voice Assistant Testing

97
from PramodDutta/qaskills

Testing voice-activated applications including speech recognition accuracy, intent detection, dialog flow testing, and multi-language support.