Guide

API Testing Automation: REST, GraphQL & SOAP Complete Guide

By Pavel Borji··Founder @ Assrt

APIs carry more than 80% of all web traffic today. When an API endpoint breaks, the ripple effect reaches every consumer, from mobile apps to partner integrations. This guide walks through a comprehensive approach to automating API tests across REST, GraphQL, and SOAP services, using Playwright and modern tooling to catch regressions before they reach production.

More

Teams that invest in API testing typically catch many more bugs before release than teams relying on UI-only testing strategies.

1. Why API Testing Is Critical

Modern applications are built on layers of APIs. A single user action, such as placing an order, can trigger dozens of API calls across payment gateways, inventory services, notification systems, and analytics pipelines. When any one of those endpoints returns an unexpected response, the entire flow can collapse silently or, worse, corrupt data in ways that surface hours later.

API testing sits at the sweet spot of the testing pyramid. It is faster than end-to-end browser tests because there is no rendering overhead, yet it validates the core business logic that unit tests often miss because they operate in isolation. A well-structured API test suite can execute hundreds of scenarios in seconds, giving your team rapid feedback on every commit.

There are several reasons API testing deserves priority in your quality strategy. First, APIs are the backbone of microservices architectures; they define the contracts between services and any deviation from that contract breaks downstream consumers. Second, API tests catch bugs before the UI layer even exists, enabling backend teams to ship confidently while frontend development proceeds in parallel. Third, API tests are inherently more stable than UI tests because they are not affected by CSS changes, layout shifts, or animation timing.

Organizations that adopt API testing early also benefit from better documentation. When your tests describe the expected request and response shapes, they serve as living documentation that stays in sync with the actual implementation. This reduces onboarding time for new developers and eliminates the drift between spec documents and production behavior.

The cost of finding a bug in production is 10 to 100 times higher than finding it during development. API testing shortens that feedback loop dramatically, catching contract violations, missing fields, incorrect status codes, and security vulnerabilities before they ever reach staging.

2. REST API Testing Fundamentals

REST (Representational State Transfer) remains the most widely used API paradigm. Testing REST APIs effectively requires a solid understanding of HTTP methods, status codes, headers, and response body validation.

HTTP Methods and Their Testing Implications

Each HTTP method carries semantic meaning that your tests should validate. GET requests must be idempotent, meaning repeated calls should return the same result without side effects. POST requests create resources and should return 201 status codes with a Location header pointing to the new resource. PUT replaces a resource entirely, while PATCH applies partial updates. DELETE should return 204 on success and subsequent GET requests for the same resource should return 404.

Your test suite should verify that each endpoint respects the HTTP method semantics. A common bug is a GET endpoint that modifies server state, which violates REST principles and can cause issues with caching layers and CDNs.

Status Code Validation

Status codes are the first line of defense in API testing. Your tests should validate not just success cases (200, 201, 204) but also error scenarios. Send malformed JSON and expect 400. Request a resource without authentication and expect 401. Access a resource without proper permissions and expect 403. Request a non-existent resource and expect 404. Send a request that violates business rules and expect 422.

Response Body and Schema Validation

Beyond status codes, validating the response body structure prevents subtle bugs from reaching consumers. Schema validation ensures that every field has the expected type, required fields are always present, and no unexpected fields leak sensitive data. Tools like JSON Schema, Zod, or Ajv can validate response shapes programmatically.

rest-api.spec.ts
import { test, expect } from '@playwright/test';

test.describe('REST API: Users', () => {
  let apiContext;

  test.beforeAll(async ({ playwright }) => {
    apiContext = await playwright.request.newContext({
      baseURL: 'https://api.example.com/v1',
      extraHTTPHeaders: {
        'Authorization': 'Bearer ' + process.env.API_TOKEN,
        'Content-Type': 'application/json',
      },
    });
  });

  test('GET /users returns paginated list', async () => {
    const response = await apiContext.get('/users?page=1&limit=10');
    expect(response.status()).toBe(200);

    const body = await response.json();
    expect(body.data).toBeInstanceOf(Array);
    expect(body.data.length).toBeLessThanOrEqual(10);
    expect(body.meta).toHaveProperty('totalCount');
    expect(body.meta).toHaveProperty('currentPage', 1);

    // Schema validation: every user has required fields
    for (const user of body.data) {
      expect(user).toHaveProperty('id');
      expect(user).toHaveProperty('email');
      expect(typeof user.email).toBe('string');
      expect(user.email).toMatch(/@/);
    }
  });

  test('POST /users creates a new user', async () => {
    const payload = {
      name: 'Test User',
      email: `test-${Date.now()}@example.com`,
      role: 'viewer',
    };

    const response = await apiContext.post('/users', { data: payload });
    expect(response.status()).toBe(201);

    const created = await response.json();
    expect(created.name).toBe(payload.name);
    expect(created.email).toBe(payload.email);
    expect(created).toHaveProperty('id');
  });

  test('GET /users/invalid returns 404', async () => {
    const response = await apiContext.get('/users/nonexistent-id');
    expect(response.status()).toBe(404);
  });

  test.afterAll(async () => {
    await apiContext.dispose();
  });
});

Notice how the test validates the full response shape, not just the status code. This approach catches regressions such as missing pagination metadata, incorrect field types, or accidentally removed properties that downstream clients depend on.

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

3. GraphQL Testing Strategies

GraphQL introduces a different testing paradigm compared to REST. Instead of testing fixed endpoints, you test queries and mutations against a schema that clients can query flexibly. This flexibility is powerful but also introduces unique testing challenges.

Query and Mutation Testing

Every GraphQL operation should be tested with both valid and invalid inputs. For queries, verify that the response matches the requested fields exactly (no over-fetching or under-fetching). For mutations, verify that the operation produces the expected side effect and returns the correct data. Pay special attention to nullable fields; a field that returns null unexpectedly can crash client applications that do not handle null gracefully.

Schema Introspection Testing

GraphQL schemas are self-documenting through introspection. Your test suite should include introspection queries that verify the schema has not changed unexpectedly. This acts as a contract test: if a field is removed or its type changes, the introspection test fails before any client code breaks. You can snapshot the schema and compare it against the current version to detect unintended breaking changes.

Variable Handling and Error Responses

GraphQL variables allow clients to parameterize queries safely. Test that variables are validated correctly: missing required variables should return a clear error, wrong types should be rejected, and injected payloads should be sanitized. Error responses in GraphQL are structured differently from REST; they appear in an errors array alongside partial data. Your tests should verify that error messages are helpful without leaking internal implementation details.

graphql-api.spec.ts
import { test, expect } from '@playwright/test';

test.describe('GraphQL API', () => {
  let apiContext;

  test.beforeAll(async ({ playwright }) => {
    apiContext = await playwright.request.newContext({
      baseURL: 'https://api.example.com',
      extraHTTPHeaders: {
        'Authorization': 'Bearer ' + process.env.API_TOKEN,
        'Content-Type': 'application/json',
      },
    });
  });

  test('query users with pagination', async () => {
    const query = `
      query GetUsers($first: Int!, $after: String) {
        users(first: $first, after: $after) {
          edges {
            node { id name email role }
            cursor
          }
          pageInfo { hasNextPage endCursor }
        }
      }
    `;

    const response = await apiContext.post('/graphql', {
      data: {
        query,
        variables: { first: 5 },
      },
    });

    expect(response.status()).toBe(200);
    const { data, errors } = await response.json();
    expect(errors).toBeUndefined();
    expect(data.users.edges.length).toBeLessThanOrEqual(5);

    for (const edge of data.users.edges) {
      expect(edge.node).toHaveProperty('id');
      expect(edge.node).toHaveProperty('email');
      expect(edge).toHaveProperty('cursor');
    }
  });

  test('mutation with invalid input returns errors', async () => {
    const mutation = `
      mutation CreateUser($input: CreateUserInput!) {
        createUser(input: $input) {
          id name email
        }
      }
    `;

    const response = await apiContext.post('/graphql', {
      data: {
        query: mutation,
        variables: { input: { name: '', email: 'not-an-email' } },
      },
    });

    const body = await response.json();
    expect(body.errors).toBeDefined();
    expect(body.errors.length).toBeGreaterThan(0);
    // Errors should not expose internal stack traces
    expect(JSON.stringify(body.errors)).not.toContain('stackTrace');
  });

  test.afterAll(async () => {
    await apiContext.dispose();
  });
});

GraphQL tests should cover both the happy path and validation errors. Since GraphQL always returns 200 for application-level errors, you must inspect the response body to detect failures, not just the status code.

4. SOAP API Testing

SOAP (Simple Object Access Protocol) is still prevalent in enterprise environments, particularly in banking, healthcare, and government systems. While newer projects typically choose REST or GraphQL, testing teams frequently need to validate SOAP services as part of integration testing.

WSDL-Based Test Generation

SOAP services publish their contract through WSDL (Web Services Description Language) files. These WSDL files define every operation, its input parameters, and expected output structure. This makes SOAP inherently testable: you can parse the WSDL and generate test cases for every operation automatically. Tools like SoapUI can import a WSDL and scaffold tests, but you can also use Playwright's request context to send raw XML payloads for tighter integration with your existing test suite.

XML Validation and Namespaces

SOAP relies on XML, which introduces complexity around namespaces, schemas (XSD), and envelope structure. Your tests should validate that responses conform to the XSD schema defined in the WSDL. Namespace mismatches are a common source of integration failures; a service might return the correct data but with a different namespace prefix, causing strict XML parsers to reject the response. Always validate the namespace URI, not just the prefix.

Legacy System Considerations

Legacy SOAP services often have unique quirks: custom headers for session management, WS-Security tokens, MTOM attachments, or non-standard error formats. Your test suite should account for these by creating helper functions that handle envelope wrapping and header injection. Consider maintaining a library of SOAP envelope templates that your tests can populate with test-specific data. This approach keeps individual tests readable while handling the XML boilerplate in a shared utility layer.

5. Playwright for API Testing

Playwright is widely known for browser automation, but its built-in API testing capabilities make it a powerful tool for backend validation as well. The APIRequestContext provides a clean interface for making HTTP requests with full control over headers, body, and authentication.

APIRequestContext

Playwright exposes playwright.request.newContext() for creating API request contexts independently of any browser. This means you can run API tests without launching a browser at all, making them extremely fast. Each context maintains its own cookie jar and headers, so you can create multiple contexts to simulate different users or authentication states simultaneously.

Combining UI and API Tests

One of Playwright's unique strengths is the ability to combine browser interactions with API calls in the same test. You can use API calls to set up test data (creating users, seeding databases), perform a browser-based user journey, then use API calls to verify the backend state. This hybrid approach gives you the confidence of end-to-end testing with the speed of API-level setup and teardown.

For example, you might create an order via the API, navigate the browser to the order detail page to verify the UI renders correctly, then call the API again to confirm that viewing the page did not accidentally modify the order state. This pattern catches a class of bugs that pure API tests or pure UI tests would miss individually.

Request Interception and Mocking

Playwright also supports route interception, allowing you to mock API responses during browser tests. This is invaluable for testing error states, slow responses, or third-party API failures without relying on external services. You can intercept specific endpoints, modify response bodies or status codes, and verify that your application handles edge cases gracefully. This capability bridges the gap between isolated unit tests and full integration tests.

6. Authentication and Authorization Testing

Security testing is one of the highest-value applications of API testing automation. Authentication (verifying identity) and authorization (verifying permissions) must be tested at every endpoint to prevent unauthorized access.

OAuth Flow Testing

OAuth 2.0 flows involve multiple steps: authorization code exchange, token refresh, and token revocation. Each step is an API call that can fail independently. Your tests should verify the complete flow: request an authorization code, exchange it for tokens, use the access token to call a protected endpoint, refresh the token when it expires, and verify that revoked tokens are rejected immediately.

JWT Validation

JSON Web Tokens are ubiquitous in API authentication. Your tests should verify that the API rejects expired tokens, tokens with invalid signatures, tokens issued by untrusted issuers, and tokens with insufficient scopes. Also test that the API does not accept tokens in the "none" algorithm, which is a well-known JWT vulnerability that attackers exploit to bypass signature verification entirely.

Role-Based Access Control

Every endpoint should be tested with multiple user roles. Create API request contexts for each role (admin, editor, viewer, unauthenticated) and verify that each context can only access the resources and operations permitted by its role. Pay special attention to horizontal privilege escalation: a viewer should not be able to access another viewer's private data, even if both share the same role. These tests should also verify that error responses do not leak information about resources the user cannot access. For instance, returning 404 instead of 403 prevents attackers from discovering the existence of resources they cannot reach.

7. Performance and Load Considerations

While dedicated load testing tools like k6 or Gatling excel at high-concurrency scenarios, your API test suite should include basic performance assertions to catch regressions early. A simple response time check can prevent a slow database query from reaching production unnoticed.

Response Time Assertions

Add timing assertions to critical endpoints. If your SLA requires responses under 200ms, assert that in your tests. Playwright provides timing information on every response, making it straightforward to add performance gates. Track these numbers over time to spot gradual degradation before it becomes a customer complaint.

Rate Limiting Verification

If your API implements rate limiting, test it explicitly. Send requests at a rate that should trigger the limiter and verify that the API returns 429 (Too Many Requests) with appropriate Retry-After headers. Also verify that rate limits reset correctly after the window expires. Under-tested rate limiting can either allow abuse or, worse, block legitimate users.

Concurrent Request Testing

Race conditions are among the hardest bugs to find, and concurrent request testing is one of the best ways to surface them. Send multiple identical requests simultaneously and verify that the system handles them correctly. For example, two concurrent requests to withdraw money from the same account should not both succeed if the balance is insufficient for both. Playwright's Promise.all pattern makes it easy to fire multiple requests in parallel and validate the collective result.

8. CI/CD Integration

API tests deliver the most value when they run automatically on every code change. Integrating your test suite into your CI/CD pipeline ensures that no breaking change reaches production without detection.

API Test Suites in Pipelines

Structure your API tests to run in stages. Fast contract tests (schema validation, status code checks) should run first because they complete in seconds and catch the most common regressions. Slower integration tests (multi-step workflows, data consistency checks) run next. Performance tests run last or on a schedule, since they take longer and may require dedicated infrastructure. This staged approach provides fast feedback on most changes while still catching subtle issues.

Contract Testing

Contract testing verifies that the API producer and consumer agree on the interface. Tools like Pact or schema-based approaches ensure that changes to the API do not break existing consumers. In a microservices architecture, contract tests prevent the cascade of failures that occurs when one team changes an API without notifying all consumers. Run contract tests in CI for both the provider and consumer repositories so that breaking changes are detected regardless of which side changes first.

Environment-Specific Configuration

Your API tests should work across environments (dev, staging, production) with minimal configuration changes. Use environment variables for base URLs, authentication tokens, and feature flags. Playwright's configuration file supports multiple projects with different settings, making it straightforward to run the same tests against different environments from a single codebase.

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'api-staging',
      use: {
        baseURL: process.env.STAGING_API_URL
          || 'https://staging-api.example.com',
        extraHTTPHeaders: {
          'Authorization': 'Bearer ' + process.env.STAGING_TOKEN,
        },
      },
    },
    {
      name: 'api-production',
      use: {
        baseURL: process.env.PROD_API_URL
          || 'https://api.example.com',
        extraHTTPHeaders: {
          'Authorization': 'Bearer ' + process.env.PROD_TOKEN,
        },
      },
    },
  ],
  reporter: [
    ['html', { open: 'never' }],
    ['json', { outputFile: 'api-test-results.json' }],
  ],
});

With this configuration, running npx playwright test --project=api-staging targets the staging environment while the same tests can run against production with a single flag change. This flexibility ensures your API tests are portable and reusable across your entire deployment pipeline.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk