# 🔌 Building Playwright Framework Step By Step - Implementing API Fixtures

## 🎯 Introduction

API (Application Programming Interface) testing is a **fundamental aspect** of the software testing process that focuses on verifying whether APIs meet functionality, reliability, performance, and security expectations! 🚀 This type of testing is conducted at the message layer and involves sending calls to the API, getting outputs, and noting the system's response.

> 🌐 **Why API Testing Matters**: APIs are the backbone of modern applications - ensuring they work flawlessly is crucial for seamless user experiences!

### 🌟 Key Aspects of API Testing:

* 🔧 **Functionality Testing**: Ensures that the API functions correctly and delivers expected outcomes in response to specific requests
    
* 🛡️ **Reliability Testing**: Verifies that the API can be consistently called upon and delivers stable performance under various conditions
    
* ⚡ **Performance Testing**: Assesses the API's efficiency, focusing on response times, load capacity, and error rates under high traffic
    
* 🔒 **Security Testing**: Evaluates the API's defense mechanisms against unauthorized access, data breaches, and vulnerabilities
    
* 🔗 **Integration Testing**: Ensures that the API integrates seamlessly with other services, platforms, and data, providing a cohesive user experience
    

> 💡 **Key Insight**: API testing is crucial due to its ability to identify issues early in the development cycle, offering a more cost-effective and streamlined approach to ensuring software quality and security.

## ✅ Prerequisites

This article builds directly on the concepts from previous ones. To get the most out of it, you should have:

* [Initialized Playwright Framework](https://idavidov.eu/building-playwright-framework-step-by-step-initial-setup)
    
* [Created User Snippets](https://idavidov.eu/building-playwright-framework-step-by-step-create-user-snippets)
    
* [Set Environment Variables](https://idavidov.eu/building-playwright-framework-step-by-step-setup-environment-variables)
    
* [Set Design Pattern](https://idavidov.eu/building-playwright-framework-step-by-step-setup-design-pattern)
    
* [Implemented POM as Fixture and Auth User Session](https://idavidov.eu/building-playwright-framework-step-by-step-implementing-pom-as-fixture-and-auth-user-session)
    
* [Implemented UI Tests](https://idavidov.eu/building-playwright-framework-step-by-step-implementing-ui-tests)
    

## 🛠️ Implement API Fixtures

### 📦 Install `zod` Package

[Zod](https://idavidov.eu/how-to-build-a-scalable-qa-framework-with-advanced-typescript-patterns) is a TypeScript-first schema declaration and validation library that provides a powerful and elegant way to ensure data integrity throughout your application! 🎯 Unlike traditional validation libraries that solely focus on runtime validation, Zod integrates seamlessly with TypeScript, offering compile-time checks and type inference. This dual approach not only fortifies your application against incorrect data but also enhances developer experience by reducing the need for manual type definitions.

> 🎭 **Why Zod?**: Combines TypeScript's compile-time safety with runtime validation - the best of both worlds!

```bash
npm install zod
```

### 📁 Create 'api' Folder in the Fixtures Directory

This will be the **central hub** where we implement API fixtures and schema validation! 🏗️

> 🗂️ **Organization Tip**: Keeping API-related code in a dedicated folder improves maintainability and code organization.

### 🔧 Create 'plain-function.ts' File

In this file, we'll **encapsulate the API request process**, managing all the necessary preparations before the request is sent and processing actions required after the response is obtained! ⚙️

> 💡 **Design Pattern**: This helper function abstracts away the complexity of API requests, making your tests cleaner and more maintainable.

```typescript
import type { APIRequestContext, APIResponse } from '@playwright/test';

/**
 * Simplified helper for making API requests and returning the status and JSON body.
 * This helper automatically performs the request based on the provided method, URL, body, and headers.
 *
 * @param {Object} params - The parameters for the request.
 * @param {APIRequestContext} params.request - The Playwright request object, used to make the HTTP request.
 * @param {string} params.method - The HTTP method to use (POST, GET, PUT, DELETE).
 * @param {string} params.url - The URL to send the request to.
 * @param {string} [params.baseUrl] - The base URL to prepend to the request URL.
 * @param {Record<string, unknown> | null} [params.body=null] - The body to send with the request (for POST and PUT requests).
 * @param {Record<string, string> | undefined} [params.headers=undefined] - The headers to include with the request.
 * @returns {Promise<{ status: number; body: unknown }>} - An object containing the status code and the parsed response body.
 *    - `status`: The HTTP status code returned by the server.
 *    - `body`: The parsed JSON response body from the server.
 */
export async function apiRequest({
    request,
    method,
    url,
    baseUrl,
    body = null,
    headers,
}: {
    request: APIRequestContext;
    method: 'POST' | 'GET' | 'PUT' | 'DELETE';
    url: string;
    baseUrl?: string;
    body?: Record<string, unknown> | null;
    headers?: string;
}): Promise<{ status: number; body: unknown }> {
    let response: APIResponse;

    const options: {
        data?: Record<string, unknown> | null;
        headers?: Record<string, string>;
    } = {};
    if (body) options.data = body;
    if (headers) {
        options.headers = {
            Authorization: `Token ${headers}`,
            'Content-Type': 'application/json',
        };
    } else {
        options.headers = {
            'Content-Type': 'application/json',
        };
    }

    const fullUrl = baseUrl ? `${baseUrl}${url}` : url;

    switch (method.toUpperCase()) {
        case 'POST':
            response = await request.post(fullUrl, options);
            break;
        case 'GET':
            response = await request.get(fullUrl, options);
            break;
        case 'PUT':
            response = await request.put(fullUrl, options);
            break;
        case 'DELETE':
            response = await request.delete(fullUrl, options);
            break;
        default:
            throw new Error(`Unsupported HTTP method: ${method}`);
    }

    const status = response.status();

    let bodyData: unknown = null;
    const contentType = response.headers()['content-type'] || '';

    try {
        if (contentType.includes('application/json')) {
            bodyData = await response.json();
        } else if (contentType.includes('text/')) {
            bodyData = await response.text();
        }
    } catch (err) {
        console.warn(
            `Failed to parse response body for status ${status}: ${err}`
        );
    }

    return { status, body: bodyData };
}
```

You can learn more about the used [Custom Types](https://idavidov.eu/a-practical-guide-to-typescript-custom-types-for-qa-automation) and [\`try/catch\` block](https://idavidov.eu/stop-writing-flaky-tests-your-foundational-guide-to-async-in-playwright) in TypeScript.

### 📋 Create `schemas.ts` File

In this file we will define **all schemas** by utilizing the powerful Zod schema validation library! 🎯

> 🛡️ **Schema Benefits**: Schemas ensure data consistency and catch type mismatches early, preventing runtime errors.

```typescript
import { z } from 'zod';

export const UserSchema = z.object({
    user: z.object({
        email: z.string().email(),
        username: z.string(),
        bio: z.string().nullable(),
        image: z.string().nullable(),
        token: z.string(),
    }),
});

export const ErrorResponseSchema = z.object({
    errors: z.object({
        email: z.array(z.string()).optional(),
        username: z.array(z.string()).optional(),
        password: z.array(z.string()).optional(),
    }),
});

export const ArticleResponseSchema = z.object({
    article: z.object({
        slug: z.string(),
        title: z.string(),
        description: z.string(),
        body: z.string(),
        tagList: z.array(z.string()),
        createdAt: z.string(),
        updatedAt: z.string(),
        favorited: z.boolean(),
        favoritesCount: z.number(),
        author: z.object({
            username: z.string(),
            bio: z.string().nullable(),
            image: z.string(),
            following: z.boolean(),
        }),
    }),
});
```

### 🔍 Create `types-guards.ts` File

In this file, we're specifying the **types essential for API Fixtures**, as well as the types corresponding to various API responses we anticipate encountering throughout testing! 📊 We are using [advanced patterns as Zod, z.infer and typeof](https://idavidov.eu/how-to-build-a-scalable-qa-framework-with-advanced-typescript-patterns)

> 🎯 **TypeScript Power**: Strong typing helps catch errors at compile time and provides excellent IDE support with autocomplete.

```typescript
import { z } from 'zod';
import type {
    UserSchema,
    ErrorResponseSchema,
    ArticleResponseSchema,
} from './schemas';

/**
 * Parameters for making an API request.
 * @typedef {Object} ApiRequestParams
 * @property {'POST' | 'GET' | 'PUT' | 'DELETE'} method - The HTTP method to use.
 * @property {string} url - The endpoint URL for the request.
 * @property {string} [baseUrl] - The base URL to prepend to the endpoint.
 * @property {Record<string, unknown> | null} [body] - The request payload, if applicable.
 * @property {string} [headers] - Additional headers for the request.
 */
export type ApiRequestParams = {
    method: 'POST' | 'GET' | 'PUT' | 'DELETE';
    url: string;
    baseUrl?: string;
    body?: Record<string, unknown> | null;
    headers?: string;
};

/**
 * Response from an API request.
 * @template T
 * @typedef {Object} ApiRequestResponse
 * @property {number} status - The HTTP status code of the response.
 * @property {T} body - The response body.
 */
export type ApiRequestResponse<T = unknown> = {
    status: number;
    body: T;
};

// define the function signature as a type
export type ApiRequestFn = <T = unknown>(
    params: ApiRequestParams
) => Promise<ApiRequestResponse<T>>;

// grouping them all together
export type ApiRequestMethods = {
    apiRequest: ApiRequestFn;
};

export type User = z.infer<typeof UserSchema>;
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
export type ArticleResponse = z.infer<typeof ArticleResponseSchema>;
```

### 🎭 Create `api-request-fixtures.ts` File

In this file we **extend the** `test` fixture from Playwright to implement our custom API fixture! 🚀 We rely on \[Generics\](We are using [Advanced patterns asZod, z.infer and typeof](https://idavidov.eu/how-to-build-a-scalable-qa-framework-with-advanced-typescript-patterns)) to make it reusable.

> 🔧 **Fixture Pattern**: Custom fixtures allow you to inject dependencies and setup code into your tests in a clean, reusable way.

```typescript
import { test as base } from '@playwright/test';
import { apiRequest as apiRequestOriginal } from './plain-function';
import {
    ApiRequestFn,
    ApiRequestMethods,
    ApiRequestParams,
    ApiRequestResponse,
} from './types-guards';

export const test = base.extend<ApiRequestMethods>({
    /**
     * Provides a function to make API requests.
     *
     * @param {object} request - The request object.
     * @param {function} use - The use function to provide the API request function.
     */
    apiRequest: async ({ request }, use) => {
        const apiRequestFn: ApiRequestFn = async <T = unknown>({
            method,
            url,
            baseUrl,
            body = null,
            headers,
        }: ApiRequestParams): Promise<ApiRequestResponse<T>> => {
            const response = await apiRequestOriginal({
                request,
                method,
                url,
                baseUrl,
                body,
                headers,
            });

            return {
                status: response.status,
                body: response.body as T,
            };
        };

        await use(apiRequestFn);
    },
});
```

### 🔄 Update `test-options.ts` File

We need to **add the API fixtures** to the file, so we can use it in our test cases! 🎯

> 🔗 **Integration**: Merging fixtures allows you to use both page objects and API utilities in the same test seamlessly.

```typescript
import { test as base, mergeTests, request } from '@playwright/test';
import { test as pageObjectFixture } from './page-object-fixture';
import { test as apiRequestFixture } from '../api/api-request-fixture';

const test = mergeTests(pageObjectFixture, apiRequestFixture);

const expect = base.expect;
export { test, expect, request };
```

## 🎯 What's Next?

In the next article we will implement **API Tests** - putting our fixtures to work with real testing scenarios! 🚀

> 💬 **Community**: Please feel free to initiate discussions on this topic, as every contribution has the potential to drive further refinement.

---

✨ **Ready to enhance your testing capabilities?** Let's continue building this robust framework together!

---

> **🙏🏻 Thank you for reading!** Building robust, scalable automation frameworks is a journey best taken together. If you found this article helpful, consider joining a growing community of QA professionals 🚀 who are passionate about mastering modern testing.

> [Join the community and get the latest articles and tips by signing up for the newsletter.](https://idavidov.eu/newsletter)
