Back to skills

Route Tester

Framework-agnostic HTTP API route testing patterns, authentication strategies, and integration testing best practices. Supports REST APIs with JWT cookie authentication and other common auth patterns.

2 stars
0 votes
0 copies
0 views
Added 12/19/2025
developmenttypescriptgosqlreactnextjsnodeexpressfastapidjangoflask

Works with

cliapi
Download Zip
Files
SKILL.md
---
name: route-tester
displayName: API Route Testing
description: Framework-agnostic HTTP API route testing patterns, authentication strategies, and integration testing best practices. Supports REST APIs with JWT cookie authentication and other common auth patterns.
---

# API Route Testing Skill

This skill provides framework-agnostic guidance for testing HTTP API routes and endpoints across any backend framework (Express, Next.js API Routes, FastAPI, Django REST, Flask, etc.).

## Core Testing Principles

### 1. Test Types for API Routes

**Unit Tests**
- Test individual route handlers in isolation
- Mock dependencies (database, external APIs)
- Fast execution (< 50ms per test)
- Focus on business logic

**Integration Tests**
- Test full request/response cycle
- Real database (test instance)
- Authentication flow included
- Slower but more comprehensive

**End-to-End Tests**
- Test from client perspective
- Full authentication flow
- Real services (or close replicas)
- Most realistic, slowest execution

### 2. Authentication Testing Patterns

#### JWT Cookie Authentication
```typescript
// Common pattern across frameworks
describe('Protected Route Tests', () => {
  let authCookie: string;

  beforeEach(async () => {
    // Login and get JWT cookie
    const loginResponse = await request(app)
      .post('/api/auth/login')
      .send({ email: 'test@example.com', password: 'password123' });

    authCookie = loginResponse.headers['set-cookie'][0];
  });

  it('should access protected route with valid cookie', async () => {
    const response = await request(app)
      .get('/api/protected/resource')
      .set('Cookie', authCookie);

    expect(response.status).toBe(200);
  });

  it('should reject access without cookie', async () => {
    const response = await request(app)
      .get('/api/protected/resource');

    expect(response.status).toBe(401);
  });
});
```

#### JWT Bearer Token Authentication
```typescript
describe('Bearer Token Auth', () => {
  let token: string;

  beforeEach(async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({ email: 'test@example.com', password: 'password123' });

    token = response.body.token;
  });

  it('should authenticate with bearer token', async () => {
    const response = await request(app)
      .get('/api/protected/resource')
      .set('Authorization', `Bearer ${token}`);

    expect(response.status).toBe(200);
  });
});
```

### 3. HTTP Method Testing

**GET Requests**
```typescript
describe('GET /api/users', () => {
  it('should return paginated users', async () => {
    const response = await request(app)
      .get('/api/users?page=1&limit=10');

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('data');
    expect(response.body).toHaveProperty('pagination');
    expect(Array.isArray(response.body.data)).toBe(true);
  });

  it('should filter users by query params', async () => {
    const response = await request(app)
      .get('/api/users?role=admin');

    expect(response.status).toBe(200);
    expect(response.body.data.every(u => u.role === 'admin')).toBe(true);
  });
});
```

**POST Requests**
```typescript
describe('POST /api/users', () => {
  it('should create new user with valid data', async () => {
    const newUser = {
      name: 'John Doe',
      email: 'john@example.com',
      role: 'user'
    };

    const response = await request(app)
      .post('/api/users')
      .set('Cookie', authCookie)
      .send(newUser);

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject(newUser);
    expect(response.body).toHaveProperty('id');
  });

  it('should reject invalid data', async () => {
    const invalidUser = {
      name: 'John Doe'
      // Missing required email field
    };

    const response = await request(app)
      .post('/api/users')
      .set('Cookie', authCookie)
      .send(invalidUser);

    expect(response.status).toBe(400);
    expect(response.body).toHaveProperty('errors');
  });
});
```

**PUT/PATCH Requests**
```typescript
describe('PATCH /api/users/:id', () => {
  it('should update user fields', async () => {
    const updates = { name: 'Jane Doe' };

    const response = await request(app)
      .patch('/api/users/123')
      .set('Cookie', authCookie)
      .send(updates);

    expect(response.status).toBe(200);
    expect(response.body.name).toBe('Jane Doe');
  });

  it('should return 404 for non-existent user', async () => {
    const response = await request(app)
      .patch('/api/users/999999')
      .set('Cookie', authCookie)
      .send({ name: 'Test' });

    expect(response.status).toBe(404);
  });
});
```

**DELETE Requests**
```typescript
describe('DELETE /api/users/:id', () => {
  it('should delete user and return success', async () => {
    const response = await request(app)
      .delete('/api/users/123')
      .set('Cookie', authCookie);

    expect(response.status).toBe(204);
  });

  it('should prevent unauthorized deletion', async () => {
    const response = await request(app)
      .delete('/api/users/123');
      // No auth cookie

    expect(response.status).toBe(401);
  });
});
```

### 4. Response Validation

**Status Codes**
```typescript
describe('HTTP Status Codes', () => {
  it('200 OK - Successful GET', async () => {
    const response = await request(app).get('/api/users');
    expect(response.status).toBe(200);
  });

  it('201 Created - Successful POST', async () => {
    const response = await request(app).post('/api/users').send(validData);
    expect(response.status).toBe(201);
  });

  it('204 No Content - Successful DELETE', async () => {
    const response = await request(app).delete('/api/users/123');
    expect(response.status).toBe(204);
  });

  it('400 Bad Request - Invalid input', async () => {
    const response = await request(app).post('/api/users').send({});
    expect(response.status).toBe(400);
  });

  it('401 Unauthorized - Missing auth', async () => {
    const response = await request(app).get('/api/protected');
    expect(response.status).toBe(401);
  });

  it('403 Forbidden - Insufficient permissions', async () => {
    const response = await request(app).delete('/api/admin/users/123').set('Cookie', userCookie);
    expect(response.status).toBe(403);
  });

  it('404 Not Found - Non-existent resource', async () => {
    const response = await request(app).get('/api/users/999999');
    expect(response.status).toBe(404);
  });

  it('500 Internal Server Error - Server failure', async () => {
    // Test error handling
    mockDatabase.findOne.mockRejectedValue(new Error('DB Error'));
    const response = await request(app).get('/api/users/123');
    expect(response.status).toBe(500);
  });
});
```

**Response Schema Validation**
```typescript
describe('Response Schema', () => {
  it('should match expected schema', async () => {
    const response = await request(app).get('/api/users/123');

    expect(response.body).toEqual({
      id: expect.any(String),
      name: expect.any(String),
      email: expect.any(String),
      role: expect.stringMatching(/^(user|admin)$/),
      createdAt: expect.any(String),
      updatedAt: expect.any(String)
    });
  });
});
```

### 5. Error Handling Tests

```typescript
describe('Error Handling', () => {
  it('should return structured error response', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ invalid: 'data' });

    expect(response.status).toBe(400);
    expect(response.body).toEqual({
      error: expect.any(String),
      message: expect.any(String),
      errors: expect.any(Array)
    });
  });

  it('should handle database errors gracefully', async () => {
    mockDatabase.findOne.mockRejectedValue(new Error('Connection lost'));

    const response = await request(app).get('/api/users/123');

    expect(response.status).toBe(500);
    expect(response.body.error).toBe('Internal Server Error');
  });

  it('should sanitize error messages in production', async () => {
    process.env.NODE_ENV = 'production';

    const response = await request(app).get('/api/error-prone-route');

    expect(response.status).toBe(500);
    expect(response.body.message).not.toContain('stack trace');
    expect(response.body.message).not.toContain('SQL');
  });
});
```

### 6. Test Setup and Teardown

```typescript
describe('API Tests', () => {
  let testDatabase;

  beforeAll(async () => {
    // Initialize test database
    testDatabase = await initTestDatabase();
  });

  afterAll(async () => {
    // Clean up test database
    await testDatabase.close();
  });

  beforeEach(async () => {
    // Seed test data
    await testDatabase.seed();
  });

  afterEach(async () => {
    // Clear test data
    await testDatabase.clear();
  });

  // Tests...
});
```

## Framework-Specific Testing Libraries

While this skill provides framework-agnostic patterns, here are common testing libraries per framework:

- **Express**: supertest, jest, vitest
- **Next.js API Routes**: @testing-library/react, next-test-api-route-handler
- **FastAPI**: pytest, httpx
- **Django REST**: django.test.TestCase, rest_framework.test
- **Flask**: pytest, flask.testing

## Best Practices

1. **Use descriptive test names** - Test names should describe the scenario and expected outcome
2. **Test happy path and edge cases** - Cover both success and failure scenarios
3. **Isolate tests** - Each test should be independent and not rely on other tests
4. **Use realistic test data** - Test data should mimic production data
5. **Clean up after tests** - Always reset state between tests
6. **Mock external dependencies** - Don't call real external APIs in tests
7. **Test authentication edge cases** - Expired tokens, invalid tokens, missing tokens
8. **Validate response schemas** - Ensure APIs return expected structure
9. **Test rate limiting** - Verify rate limits work correctly
10. **Test CORS headers** - Ensure CORS is configured correctly

## Common Pitfalls

❌ **Don't share state between tests**
```typescript
// Bad
let userId;
it('creates user', async () => {
  const response = await request(app).post('/api/users').send(userData);
  userId = response.body.id; // Shared state!
});

it('deletes user', async () => {
  await request(app).delete(`/api/users/${userId}`); // Depends on previous test
});
```

✅ **Do create fresh state for each test**
```typescript
// Good
it('creates user', async () => {
  const response = await request(app).post('/api/users').send(userData);
  expect(response.status).toBe(201);
});

it('deletes user', async () => {
  const user = await createTestUser();
  const response = await request(app).delete(`/api/users/${user.id}`);
  expect(response.status).toBe(204);
});
```

## Additional Resources

See the `resources/` directory for more detailed guides:
- `http-testing-fundamentals.md` - Deep dive into HTTP testing concepts
- `authentication-testing.md` - Authentication strategies and edge cases
- `api-integration-testing.md` - Integration testing patterns and tools

## Quick Reference

**Test Structure**
```typescript
describe('Resource Name', () => {
  describe('HTTP Method /path', () => {
    it('should describe expected behavior', async () => {
      // Arrange
      const testData = {...};

      // Act
      const response = await request(app)
        .method('/path')
        .set('Cookie', authCookie)
        .send(testData);

      // Assert
      expect(response.status).toBe(expectedStatus);
      expect(response.body).toMatchObject(expectedData);
    });
  });
});
```

**Authentication Pattern**
```typescript
let authCookie: string;

beforeEach(async () => {
  const response = await request(app)
    .post('/api/auth/login')
    .send({ email: 'test@example.com', password: 'password123' });

  authCookie = response.headers['set-cookie'][0];
});

// Use authCookie in protected route tests
.set('Cookie', authCookie)
```

Comments (0)

No comments yet. Be the first to comment!