Skip to main content
/tayyab/portfolio — zsh
tayyab
TA
// dispatch.read --classified=false --access-level: public

API Testing Complete Guide: REST, GraphQL, Postman, and Playwright (2026)

March 7, 2026 EST. READ: 20 MIN #Quality Assurance

API testing is the most underrated skill in QA.

Most teams focus on UI testing (Selenium, Playwright). But 60% of bugs live in the API layer—authentication failures, data corruption, race conditions—invisible in UI tests.

I've tested APIs across 12 projects: fintech payment systems, healthcare data pipelines, AI agent backends. This guide covers everything I've learned.

Why API Testing Matters (And Why You're Probably Neglecting It)

The Numbers:

  • UI tests: 1 endpoint, multiple steps, 30 seconds per test
  • API tests: 100 endpoints, isolated, 1 second per test
  • Coverage: Same feature coverage in 1/30th the time

Real Example (AI Sales Assistant):

  • UI test: Login → Browse products → Add to cart → Checkout (4 minutes)
  • API test: POST /checkout with payload validation (10 seconds)
  • Same coverage. 24x faster.

API Testing Fundamentals

What Are You Actually Testing?

APIs are contracts between client (frontend) and server (backend).

Client sends:     GET /api/users/1
Server responds:  {"id": 1, "name": "John", "email": "john@example.com"}

API Test checks:
✅ Status code = 200
✅ Response has required fields
✅ Data types are correct (id is number, not string)
✅ No extra fields (security)

The Three Layers of API Testing

Layer 1: Contract Testing

  • Does the API respond at all?
  • Is the HTTP status correct? (200, 201, 400, 401, 500)
  • Does the response have the right structure?
test('GET /users/:id returns correct structure', async ({ request }) => {
  const response = await request.get('/api/users/1');
  
  expect(response.status()).toBe(200);
  
  const user = await response.json();
  expect(user).toHaveProperty('id');
  expect(user).toHaveProperty('name');
  expect(user).toHaveProperty('email');
  expect(typeof user.id).toBe('number');
});

Layer 2: Business Logic Testing

  • Does the API calculate/return the right values?
  • Are edge cases handled? (0, negative, null, empty)
  • Do dependent APIs work together?
test('Lead qualification API returns correct score', async ({ request }) => {
  const response = await request.post('/api/qualify-lead', {
    data: {
      name: 'Acme Corp',
      employees: 500,
      revenue: '$10M',
      industry: 'fintech'
    }
  });
  
  const qualification = await response.json();
  expect(qualification.score).toBeGreaterThan(0.8); // Should be hot lead
  expect(qualification.nextStep).toBe('demo');
});

Layer 3: Integration Testing

  • Does API A work with Database B and Service C?
  • Are transactions atomic? (All-or-nothing)
  • Are race conditions possible?
test('Create user → Assign permissions → Verify in database', async ({ request }) => {
  // Step 1: Create user
  const createResponse = await request.post('/api/users', {
    data: { name: 'John', email: 'john@example.com' }
  });
  const userId = (await createResponse.json()).id;
  
  // Step 2: Assign permissions
  await request.post(`/api/users/${userId}/permissions`, {
    data: { role: 'admin' }
  });
  
  // Step 3: Verify in database (via separate API call)
  const userResponse = await request.get(`/api/users/${userId}`);
  const user = await userResponse.json();
  expect(user.role).toBe('admin');
});

Testing REST APIs

HTTP Methods & What to Test

Method Purpose Test What
GET Read data Status 200, correct data, no side effects
POST Create data Status 201, ID returned, data persisted
PUT Replace data Status 200, all fields updated, old data gone
PATCH Partial update Status 200, only sent fields updated, others unchanged
DELETE Remove data Status 204 or 200, data actually deleted

Testing CRUD Operations (Create, Read, Update, Delete)

test.describe('User CRUD', () => {
  let userId: number;
  
  // CREATE
  test('POST /users creates new user', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: { name: 'John Doe', email: 'john@example.com' }
    });
    
    expect(response.status()).toBe(201);
    const user = await response.json();
    userId = user.id;
    expect(userId).toBeDefined();
  });
  
  // READ
  test('GET /users/:id retrieves user', async ({ request }) => {
    const response = await request.get(`/api/users/${userId}`);
    
    expect(response.status()).toBe(200);
    const user = await response.json();
    expect(user.name).toBe('John Doe');
  });
  
  // UPDATE
  test('PATCH /users/:id updates user', async ({ request }) => {
    const response = await request.patch(`/api/users/${userId}`, {
      data: { name: 'Jane Doe' }
    });
    
    expect(response.status()).toBe(200);
    const user = await response.json();
    expect(user.name).toBe('Jane Doe');
    expect(user.email).toBe('john@example.com'); // Unchanged
  });
  
  // DELETE
  test('DELETE /users/:id removes user', async ({ request }) => {
    const response = await request.delete(`/api/users/${userId}`);
    
    expect(response.status()).toBe(204); // No content
    
    // Verify deletion
    const getResponse = await request.get(`/api/users/${userId}`);
    expect(getResponse.status()).toBe(404); // Not found
  });
});

Authentication & Authorization Testing

Testing Protected Endpoints

test.describe('Authentication', () => {
  let authToken: string;
  
  // Get auth token
  test.beforeAll(async ({ request }) => {
    const response = await request.post('/api/auth/login', {
      data: {
        email: 'user@example.com',
        password: 'password123'
      }
    });
    
    const data = await response.json();
    authToken = data.token;
  });
  
  // Test protected endpoint
  test('GET /api/profile requires authentication', async ({ request }) => {
    // Without token
    const noAuthResponse = await request.get('/api/profile');
    expect(noAuthResponse.status()).toBe(401); // Unauthorized
    
    // With token
    const withAuthResponse = await request.get('/api/profile', {
      headers: { 'Authorization': `Bearer ${authToken}` }
    });
    expect(withAuthResponse.status()).toBe(200);
  });
  
  // Test invalid token
  test('Invalid token should fail', async ({ request }) => {
    const response = await request.get('/api/profile', {
      headers: { 'Authorization': 'Bearer invalid-token' }
    });
    
    expect(response.status()).toBe(401);
  });
});

Testing GraphQL APIs

GraphQL Query Testing

test('GraphQL query for user data', async ({ request }) => {
  const response = await request.post('/graphql', {
    data: {
      query: `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            name
            email
            posts {
              id
              title
            }
          }
        }
      `,
      variables: { id: '1' }
    }
  });
  
  expect(response.status()).toBe(200);
  
  const result = await response.json();
  expect(result.data.user.name).toBeDefined();
  expect(result.data.user.posts).toBeInstanceOf(Array);
});

test('GraphQL mutation to create post', async ({ request }) => {
  const response = await request.post('/graphql', {
    data: {
      query: `
        mutation CreatePost($title: String!, $content: String!) {
          createPost(title: $title, content: $content) {
            id
            title
            createdAt
          }
        }
      `,
      variables: {
        title: 'My First Post',
        content: 'Post content here'
      }
    }
  });
  
  expect(response.status()).toBe(200);
  
  const result = await response.json();
  expect(result.data.createPost.id).toBeDefined();
  expect(result.data.createPost.title).toBe('My First Post');
});

Error Handling & Status Codes

Testing Error Responses

test.describe('Error Handling', () => {
  // 400: Bad Request
  test('Missing required field returns 400', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: {
        name: 'John'
        // Missing: email
      }
    });
    
    expect(response.status()).toBe(400);
    const error = await response.json();
    expect(error.message).toContain('email is required');
  });
  
  // 401: Unauthorized
  test('Protected endpoint without auth returns 401', async ({ request }) => {
    const response = await request.get('/api/admin/users');
    expect(response.status()).toBe(401);
  });
  
  // 403: Forbidden
  test('Non-admin cannot access admin endpoint', async ({ request }) => {
    const response = await request.get('/api/admin/users', {
      headers: { 'Authorization': 'Bearer user-token' } // Regular user
    });
    expect(response.status()).toBe(403); // Forbidden
  });
  
  // 404: Not Found
  test('Non-existent user returns 404', async ({ request }) => {
    const response = await request.get('/api/users/999999');
    expect(response.status()).toBe(404);
  });
  
  // 409: Conflict
  test('Duplicate email returns 409', async ({ request }) => {
    // Create user
    await request.post('/api/users', {
      data: { name: 'John', email: 'john@example.com' }
    });
    
    // Try to create with same email
    const response = await request.post('/api/users', {
      data: { name: 'Jane', email: 'john@example.com' }
    });
    
    expect(response.status()).toBe(409); // Conflict
  });
  
  // 500: Internal Server Error
  test('Server error returns 500', async ({ request }) => {
    const response = await request.get('/api/broken-endpoint');
    expect(response.status()).toBe(500);
    expect(response.status()).toBeGreaterThanOrEqual(500);
  });
});

API Testing with Postman

Postman Workflow

  1. Create Collection: Group related requests (Users, Products, Orders)
  2. Set Variables: Base URL, auth tokens, test data
  3. Write Tests: Assertions in "Tests" tab
  4. Run Collection: Execute all requests sequentially

Example Postman Test

// In Postman's "Tests" tab
pm.test(\"Status code is 200\", function() {
  pm.response.to.have.status(200);
});

pm.test(\"Response has user data\", function() {
  var jsonData = pm.response.json();
  pm.expect(jsonData).to.have.property('id');
  pm.expect(jsonData.email).to.equal('user@example.com');
});

pm.test(\"Response time is acceptable\", function() {
  pm.expect(pm.response.responseTime).to.be.below(1000); // < 1 second
});

// Store token for next request
pm.environment.set(\"auth_token\", pm.response.json().token);

Data Validation & Edge Cases

Testing Edge Cases

test.describe('Edge Cases', () => {
  // Null/empty values
  test('API handles empty string gracefully', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: { name: '', email: 'test@example.com' }
    });
    
    expect(response.status()).toBe(400);
    const error = await response.json();
    expect(error.message).toContain('name cannot be empty');
  });
  
  // Very large values
  test('API rejects excessively large input', async ({ request }) => {
    const largeString = 'a'.repeat(10000);
    const response = await request.post('/api/comments', {
      data: { text: largeString }
    });
    
    expect(response.status()).toBe(400);
  });
  
  // Special characters
  test('API sanitizes special characters', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: { 
        name: '',
        email: 'test@example.com'
      }
    });
    
    expect(response.status()).toBe(201);
    const user = await response.json();
    expect(user.name).not.toContain('