plugins/qa-testing/skills/api-testing/api-test-patterns/SKILL.md
Write comprehensive API tests for REST and GraphQL endpoints. Use this skill when testing APIs, writing contract tests, or validating integrations. Activate when: api testing, REST test, GraphQL test, endpoint testing, integration test, postman, contract testing.
npx skillsauth add latestaiagents/agent-skills api-test-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Write comprehensive API tests that ensure reliability and contract compliance.
// tests/api/users.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Users API', () => {
const baseURL = process.env.API_URL || 'http://localhost:3000';
test('GET /users returns list of users', async ({ request }) => {
const response = await request.get(`${baseURL}/api/users`);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const users = await response.json();
expect(Array.isArray(users)).toBeTruthy();
expect(users.length).toBeGreaterThan(0);
// Validate schema
expect(users[0]).toMatchObject({
id: expect.any(Number),
email: expect.any(String),
name: expect.any(String),
});
});
test('POST /users creates new user', async ({ request }) => {
const newUser = {
email: '[email protected]',
name: 'Test User',
password: 'securepassword123',
};
const response = await request.post(`${baseURL}/api/users`, {
data: newUser,
});
expect(response.status()).toBe(201);
const created = await response.json();
expect(created.email).toBe(newUser.email);
expect(created.name).toBe(newUser.name);
expect(created).not.toHaveProperty('password');
});
test('GET /users/:id returns single user', async ({ request }) => {
const response = await request.get(`${baseURL}/api/users/1`);
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.id).toBe(1);
});
test('PUT /users/:id updates user', async ({ request }) => {
const updates = { name: 'Updated Name' };
const response = await request.put(`${baseURL}/api/users/1`, {
data: updates,
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.name).toBe('Updated Name');
});
test('DELETE /users/:id removes user', async ({ request }) => {
const response = await request.delete(`${baseURL}/api/users/1`);
expect(response.status()).toBe(204);
// Verify deleted
const getResponse = await request.get(`${baseURL}/api/users/1`);
expect(getResponse.status()).toBe(404);
});
});
test.describe('Authentication', () => {
test('requires auth for protected endpoints', async ({ request }) => {
const response = await request.get(`${baseURL}/api/profile`);
expect(response.status()).toBe(401);
});
test('accepts valid bearer token', async ({ request }) => {
const loginResponse = await request.post(`${baseURL}/api/auth/login`, {
data: { email: '[email protected]', password: 'password' },
});
const { token } = await loginResponse.json();
const response = await request.get(`${baseURL}/api/profile`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
expect(response.ok()).toBeTruthy();
});
test('rejects expired token', async ({ request }) => {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // expired
const response = await request.get(`${baseURL}/api/profile`, {
headers: {
Authorization: `Bearer ${expiredToken}`,
},
});
expect(response.status()).toBe(401);
const body = await response.json();
expect(body.error).toContain('expired');
});
});
test.describe('Error Handling', () => {
test('returns 400 for invalid input', async ({ request }) => {
const response = await request.post(`${baseURL}/api/users`, {
data: { email: 'invalid-email' }, // missing required fields
});
expect(response.status()).toBe(400);
const error = await response.json();
expect(error).toMatchObject({
error: expect.any(String),
details: expect.any(Array),
});
});
test('returns 404 for non-existent resource', async ({ request }) => {
const response = await request.get(`${baseURL}/api/users/99999`);
expect(response.status()).toBe(404);
const error = await response.json();
expect(error.error).toContain('not found');
});
test('returns 409 for duplicate resource', async ({ request }) => {
// Create first user
await request.post(`${baseURL}/api/users`, {
data: { email: '[email protected]', name: 'User 1', password: 'pass' },
});
// Try to create duplicate
const response = await request.post(`${baseURL}/api/users`, {
data: { email: '[email protected]', name: 'User 2', password: 'pass' },
});
expect(response.status()).toBe(409);
});
test('returns 429 when rate limited', async ({ request }) => {
const requests = Array(100).fill(null).map(() =>
request.get(`${baseURL}/api/health`)
);
const responses = await Promise.all(requests);
const rateLimited = responses.some(r => r.status() === 429);
expect(rateLimited).toBeTruthy();
});
});
// tests/api/graphql.spec.ts
import { test, expect } from '@playwright/test';
test.describe('GraphQL API', () => {
const graphqlURL = `${process.env.API_URL}/graphql`;
async function graphqlRequest(request: any, query: string, variables = {}) {
return request.post(graphqlURL, {
data: { query, variables },
headers: { 'Content-Type': 'application/json' },
});
}
test('query users', async ({ request }) => {
const query = `
query GetUsers {
users {
id
name
email
}
}
`;
const response = await graphqlRequest(request, query);
const { data, errors } = await response.json();
expect(errors).toBeUndefined();
expect(data.users).toBeInstanceOf(Array);
expect(data.users[0]).toHaveProperty('id');
});
test('query with variables', async ({ request }) => {
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
const response = await graphqlRequest(request, query, { id: '1' });
const { data, errors } = await response.json();
expect(errors).toBeUndefined();
expect(data.user.id).toBe('1');
});
test('mutation creates resource', async ({ request }) => {
const mutation = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
const response = await graphqlRequest(request, mutation, {
input: {
name: 'New User',
email: '[email protected]',
password: 'password123',
},
});
const { data, errors } = await response.json();
expect(errors).toBeUndefined();
expect(data.createUser.email).toBe('[email protected]');
});
test('handles GraphQL errors', async ({ request }) => {
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`;
const response = await graphqlRequest(request, query, { id: '99999' });
const { data, errors } = await response.json();
expect(data.user).toBeNull();
expect(errors).toBeDefined();
expect(errors[0].message).toContain('not found');
});
});
// tests/contract/user-service.pact.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { resolve } from 'path';
const { like, eachLike, string, integer } = MatchersV3;
const provider = new PactV3({
consumer: 'Frontend',
provider: 'UserService',
dir: resolve(process.cwd(), 'pacts'),
});
describe('User Service Contract', () => {
it('returns users list', async () => {
// Define expected interaction
provider
.given('users exist')
.uponReceiving('a request for all users')
.withRequest({
method: 'GET',
path: '/api/users',
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: eachLike({
id: integer(1),
name: string('John Doe'),
email: string('[email protected]'),
}),
});
// Execute test
await provider.executeTest(async (mockserver) => {
const response = await fetch(`${mockserver.url}/api/users`);
const users = await response.json();
expect(users).toHaveLength(1);
expect(users[0]).toHaveProperty('id');
});
});
});
## Endpoint: [METHOD] /path
### Happy Path
- [ ] Returns correct status code
- [ ] Response body matches schema
- [ ] Required fields present
- [ ] Correct content-type header
### Authentication/Authorization
- [ ] Rejects unauthenticated requests
- [ ] Rejects unauthorized users
- [ ] Accepts valid credentials
- [ ] Handles token expiration
### Input Validation
- [ ] Rejects missing required fields
- [ ] Rejects invalid data types
- [ ] Rejects values outside constraints
- [ ] Handles empty strings/arrays
### Error Cases
- [ ] 400 for bad request
- [ ] 401 for unauthorized
- [ ] 403 for forbidden
- [ ] 404 for not found
- [ ] 409 for conflict
- [ ] 422 for validation errors
- [ ] 500 for server errors
### Edge Cases
- [ ] Empty results
- [ ] Pagination boundaries
- [ ] Special characters in input
- [ ] Maximum payload size
- [ ] Concurrent requests
### Performance
- [ ] Response time < threshold
- [ ] Handles expected load
- [ ] Rate limiting works
development
Test skills for correct activation, content quality, and regression — both automated checks (frontmatter validity, lint) and manual verification (query-suite activation testing). Covers CI integration and how to catch skill regressions before users do. Use this skill when adding skills to a repo, setting up CI for a skill library, or debugging "the skill exists but doesn't work". Activate when: test skills, validate skills, skill CI, skill linting, skill activation test, skill regression.
documentation
Write the YAML frontmatter for a SKILL.md file so it activates reliably — name, description, and activation keywords that the model matches against. Covers length, tone, and the most common frontmatter mistakes. Use this skill when authoring a new skill, fixing a skill that isn't auto-activating, or reviewing skills for publication. Activate when: SKILL.md frontmatter, skill description, skill activation, skill YAML, write a skill, author a skill.
development
Design skills that fire at the right moment — neither over-eager (noise) nor under-eager (silent). Covers activation specificity, trigger phrases, disambiguation between overlapping skills, and debugging activation. Use this skill when multiple skills could fire on the same query, a skill never fires, or a skill fires too often. Activate when: skill won't activate, skill over-activates, overlapping skills, skill triggers, skill selection, skill disambiguation.
development
Structure SKILL.md content so the model reads just enough — concise summary up front, progressively deeper detail, examples on demand. Covers section ordering, length budgets, when to split into multiple skills. Use this skill when writing or refactoring a skill body, one skill has grown too long, or a skill is wordy but not useful. Activate when: SKILL.md structure, skill content, skill too long, split skill, progressive disclosure, skill body.