API Framework Design

Building scalable API automation framework

Project Structure

A well-organized project structure ensures maintainability and scalability.

api-automation-framework/
├── src/
│   ├── main/java/
│   │   └── com/framework/
│   │       ├── config/
│   │       │   ├── ConfigReader.java
│   │       │   └── Environment.java
│   │       ├── constants/
│   │       │   └── Endpoints.java
│   │       ├── models/
│   │       │   ├── request/
│   │       │   │   └── UserRequest.java
│   │       │   └── response/
│   │       │       └── UserResponse.java
│   │       ├── specs/
│   │       │   └── RequestSpecs.java
│   │       └── utils/
│   │           ├── AuthManager.java
│   │           ├── DataGenerator.java
│   │           └── JsonUtils.java
│   └── test/
│       ├── java/
│       │   └── com/tests/
│       │       ├── base/
│       │       │   └── BaseTest.java
│       │       ├── users/
│       │       │   └── UserApiTests.java
│       │       └── auth/
│       │           └── AuthTests.java
│       └── resources/
│           ├── config/
│           │   ├── config.properties
│           │   ├── qa.properties
│           │   └── prod.properties
│           ├── schemas/
│           │   └── user-schema.json
│           └── testdata/
│               └── users.json
├── pom.xml
└── testng.xml

Configuration Management

Properties File

Create src/test/resources/config/config.properties:

# Base Configuration
base.uri=https://reqres.in
base.path=/api

# Timeouts
connection.timeout=10000
socket.timeout=10000

# Logging
log.request=true
log.response=true

# Auth
auth.username=testuser
auth.password=testpass

Environment-specific Config

qa.properties:

base.uri=https://qa-api.example.com
auth.username=qa_user

ConfigReader Class

public class ConfigReader {

    private static final Properties properties = new Properties();
    private static final String ENV = System.getProperty("env", "config");

    static {
        loadProperties();
    }

    private static void loadProperties() {
        try {
            // Load default config
            String defaultPath = "config/config.properties";
            InputStream defaultStream = ConfigReader.class.getClassLoader()
                .getResourceAsStream(defaultPath);
            properties.load(defaultStream);

            // Override with environment-specific config
            if (!ENV.equals("config")) {
                String envPath = "config/" + ENV + ".properties";
                InputStream envStream = ConfigReader.class.getClassLoader()
                    .getResourceAsStream(envPath);
                if (envStream != null) {
                    properties.load(envStream);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to load config", e);
        }
    }

    public static String get(String key) {
        return properties.getProperty(key);
    }

    public static String getBaseUri() {
        return get("base.uri");
    }

    public static String getBasePath() {
        return get("base.path");
    }

    public static int getConnectionTimeout() {
        return Integer.parseInt(get("connection.timeout"));
    }

    public static boolean logRequest() {
        return Boolean.parseBoolean(get("log.request"));
    }
}

Running with Different Environments

# Run with QA config
mvn test -Denv=qa

# Run with Production config
mvn test -Denv=prod

Endpoint Constants

Endpoints Class

public final class Endpoints {

    private Endpoints() {} // Prevent instantiation

    // User endpoints
    public static final String USERS = "/users";
    public static final String USER_BY_ID = "/users/{id}";
    public static final String USER_AVATAR = "/users/{id}/avatar";

    // Auth endpoints
    public static final String LOGIN = "/login";
    public static final String REGISTER = "/register";
    public static final String LOGOUT = "/logout";

    // Resource endpoints
    public static final String RESOURCES = "/unknown";
    public static final String RESOURCE_BY_ID = "/unknown/{id}";
}

HTTP Status Codes

public final class StatusCodes {

    private StatusCodes() {}

    public static final int OK = 200;
    public static final int CREATED = 201;
    public static final int NO_CONTENT = 204;
    public static final int BAD_REQUEST = 400;
    public static final int UNAUTHORIZED = 401;
    public static final int FORBIDDEN = 403;
    public static final int NOT_FOUND = 404;
    public static final int SERVER_ERROR = 500;
}

POJO Models

Request Models

// CreateUserRequest.java
public class CreateUserRequest {
    private String name;
    private String job;

    // Builder pattern for clean instantiation
    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private final CreateUserRequest request = new CreateUserRequest();

        public Builder name(String name) {
            request.name = name;
            return this;
        }

        public Builder job(String job) {
            request.job = job;
            return this;
        }

        public CreateUserRequest build() {
            return request;
        }
    }

    // Getters
    public String getName() { return name; }
    public String getJob() { return job; }
}

Response Models

// UserResponse.java
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserResponse {
    private UserData data;
    private Support support;

    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class UserData {
        private int id;
        private String email;
        @JsonProperty("first_name")
        private String firstName;
        @JsonProperty("last_name")
        private String lastName;
        private String avatar;

        // Getters and setters
        public int getId() { return id; }
        public String getEmail() { return email; }
        public String getFirstName() { return firstName; }
        public String getLastName() { return lastName; }
        public String getAvatar() { return avatar; }
    }

    public static class Support {
        private String url;
        private String text;

        public String getUrl() { return url; }
        public String getText() { return text; }
    }

    public UserData getData() { return data; }
    public Support getSupport() { return support; }
}

Generic List Response

// ListResponse.java - Handles paginated responses
@JsonIgnoreProperties(ignoreUnknown = true)
public class ListResponse<T> {
    private int page;
    @JsonProperty("per_page")
    private int perPage;
    private int total;
    @JsonProperty("total_pages")
    private int totalPages;
    private List<T> data;

    // Getters
    public int getPage() { return page; }
    public int getPerPage() { return perPage; }
    public int getTotal() { return total; }
    public int getTotalPages() { return totalPages; }
    public List<T> getData() { return data; }
}

Request Specifications

RequestSpecs Factory

public class RequestSpecs {

    public static RequestSpecification getDefaultSpec() {
        RequestSpecBuilder builder = new RequestSpecBuilder()
            .setBaseUri(ConfigReader.getBaseUri())
            .setBasePath(ConfigReader.getBasePath())
            .setContentType(ContentType.JSON)
            .setAccept(ContentType.JSON);

        if (ConfigReader.logRequest()) {
            builder.log(LogDetail.ALL);
        }

        return builder.build();
    }

    public static RequestSpecification getAuthenticatedSpec(String token) {
        return new RequestSpecBuilder()
            .addRequestSpecification(getDefaultSpec())
            .addHeader("Authorization", "Bearer " + token)
            .build();
    }

    public static RequestSpecification getFormSpec() {
        return new RequestSpecBuilder()
            .setBaseUri(ConfigReader.getBaseUri())
            .setBasePath(ConfigReader.getBasePath())
            .setContentType(ContentType.URLENC)
            .build();
    }

    public static RequestSpecification getMultipartSpec() {
        return new RequestSpecBuilder()
            .setBaseUri(ConfigReader.getBaseUri())
            .setBasePath(ConfigReader.getBasePath())
            .setContentType("multipart/form-data")
            .build();
    }
}

Response Specifications

public class ResponseSpecs {

    public static ResponseSpecification successSpec() {
        return new ResponseSpecBuilder()
            .expectStatusCode(StatusCodes.OK)
            .expectContentType(ContentType.JSON)
            .build();
    }

    public static ResponseSpecification createdSpec() {
        return new ResponseSpecBuilder()
            .expectStatusCode(StatusCodes.CREATED)
            .expectContentType(ContentType.JSON)
            .build();
    }

    public static ResponseSpecification notFoundSpec() {
        return new ResponseSpecBuilder()
            .expectStatusCode(StatusCodes.NOT_FOUND)
            .build();
    }

    public static ResponseSpecification unauthorizedSpec() {
        return new ResponseSpecBuilder()
            .expectStatusCode(StatusCodes.UNAUTHORIZED)
            .build();
    }
}

Base Test Class

public class BaseTest {

    protected RequestSpecification requestSpec;
    protected String authToken;

    @BeforeClass(alwaysRun = true)
    public void globalSetup() {
        // Configure Rest Assured defaults
        RestAssured.baseURI = ConfigReader.getBaseUri();
        RestAssured.basePath = ConfigReader.getBasePath();

        // Setup default request spec
        requestSpec = RequestSpecs.getDefaultSpec();

        // Enable logging on validation failure
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
    }

    @BeforeMethod
    public void testSetup(Method method) {
        System.out.println("Starting: " + method.getName());
    }

    @AfterMethod
    public void testTeardown(ITestResult result) {
        if (result.getStatus() == ITestResult.FAILURE) {
            System.out.println("Test Failed: " + result.getName());
        }
    }

    @AfterClass(alwaysRun = true)
    public void globalTeardown() {
        RestAssured.reset();
    }

    // Helper methods
    protected Response sendGet(String endpoint) {
        return given()
            .spec(requestSpec)
        .when()
            .get(endpoint);
    }

    protected Response sendPost(String endpoint, Object body) {
        return given()
            .spec(requestSpec)
            .body(body)
        .when()
            .post(endpoint);
    }

    protected Response sendPut(String endpoint, Object body) {
        return given()
            .spec(requestSpec)
            .body(body)
        .when()
            .put(endpoint);
    }

    protected Response sendDelete(String endpoint) {
        return given()
            .spec(requestSpec)
        .when()
            .delete(endpoint);
    }
}

Utility Classes

AuthManager

public class AuthManager {

    private static String cachedToken;
    private static long tokenExpiry;

    public static String getToken() {
        if (cachedToken == null || isTokenExpired()) {
            cachedToken = fetchNewToken();
            tokenExpiry = System.currentTimeMillis() + 3600000; // 1 hour
        }
        return cachedToken;
    }

    private static boolean isTokenExpired() {
        return System.currentTimeMillis() >= tokenExpiry - 60000;
    }

    private static String fetchNewToken() {
        return given()
            .spec(RequestSpecs.getFormSpec())
            .formParam("email", ConfigReader.get("auth.username"))
            .formParam("password", ConfigReader.get("auth.password"))
        .when()
            .post(Endpoints.LOGIN)
        .then()
            .statusCode(StatusCodes.OK)
            .extract().path("token");
    }

    public static void clearToken() {
        cachedToken = null;
        tokenExpiry = 0;
    }
}

DataGenerator

public class DataGenerator {

    private static final Faker faker = new Faker();

    public static CreateUserRequest randomUser() {
        return CreateUserRequest.builder()
            .name(faker.name().fullName())
            .job(faker.job().title())
            .build();
    }

    public static String randomEmail() {
        return faker.internet().emailAddress();
    }

    public static String randomName() {
        return faker.name().fullName();
    }

    public static Map<String, Object> randomUserMap() {
        Map<String, Object> user = new HashMap<>();
        user.put("name", randomName());
        user.put("job", faker.job().title());
        return user;
    }
}

JsonUtils

public class JsonUtils {

    private static final ObjectMapper mapper = new ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    public static <T> T fromJson(String json, Class<T> clazz) {
        try {
            return mapper.readValue(json, clazz);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to parse JSON", e);
        }
    }

    public static String toJson(Object object) {
        try {
            return mapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to convert to JSON", e);
        }
    }

    public static String readJsonFile(String filePath) {
        try {
            return new String(Files.readAllBytes(Paths.get(filePath)));
        } catch (IOException e) {
            throw new RuntimeException("Failed to read file: " + filePath, e);
        }
    }

    public static <T> T readJsonFileAs(String filePath, Class<T> clazz) {
        String json = readJsonFile(filePath);
        return fromJson(json, clazz);
    }
}

Complete Test Examples

User API Tests

public class UserApiTests extends BaseTest {

    @Test(description = "Verify user creation")
    public void testCreateUser() {
        CreateUserRequest user = DataGenerator.randomUser();

        sendPost(Endpoints.USERS, user)
            .then()
            .spec(ResponseSpecs.createdSpec())
            .body("name", equalTo(user.getName()))
            .body("job", equalTo(user.getJob()))
            .body("id", notNullValue())
            .body("createdAt", notNullValue());
    }

    @Test(description = "Verify get single user")
    public void testGetUser() {
        int userId = 2;

        UserResponse response = given()
            .spec(requestSpec)
            .pathParam("id", userId)
        .when()
            .get(Endpoints.USER_BY_ID)
        .then()
            .spec(ResponseSpecs.successSpec())
            .extract().as(UserResponse.class);

        Assert.assertEquals(response.getData().getId(), userId);
        Assert.assertNotNull(response.getData().getEmail());
    }

    @Test(description = "Verify user list pagination")
    public void testGetUsersList() {
        given()
            .spec(requestSpec)
            .queryParam("page", 1)
            .queryParam("per_page", 5)
        .when()
            .get(Endpoints.USERS)
        .then()
            .spec(ResponseSpecs.successSpec())
            .body("page", equalTo(1))
            .body("per_page", equalTo(5))
            .body("data.size()", lessThanOrEqualTo(5))
            .body("data.id", everyItem(greaterThan(0)));
    }

    @Test(description = "Verify update user")
    public void testUpdateUser() {
        CreateUserRequest updatedUser = CreateUserRequest.builder()
            .name("Updated Name")
            .job("Senior Engineer")
            .build();

        given()
            .spec(requestSpec)
            .pathParam("id", 2)
            .body(updatedUser)
        .when()
            .put(Endpoints.USER_BY_ID)
        .then()
            .spec(ResponseSpecs.successSpec())
            .body("name", equalTo("Updated Name"))
            .body("updatedAt", notNullValue());
    }

    @Test(description = "Verify delete user")
    public void testDeleteUser() {
        given()
            .spec(requestSpec)
            .pathParam("id", 2)
        .when()
            .delete(Endpoints.USER_BY_ID)
        .then()
            .statusCode(StatusCodes.NO_CONTENT);
    }

    @Test(description = "Verify user not found")
    public void testUserNotFound() {
        given()
            .spec(requestSpec)
            .pathParam("id", 9999)
        .when()
            .get(Endpoints.USER_BY_ID)
        .then()
            .spec(ResponseSpecs.notFoundSpec());
    }
}

Reporting Integration

Extent Reports Setup

public class ExtentReportListener implements ITestListener {

    private static ExtentReports extent;
    private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();

    @Override
    public void onStart(ITestContext context) {
        extent = new ExtentReports();
        ExtentSparkReporter spark = new ExtentSparkReporter("reports/extent-report.html");
        spark.config().setTheme(Theme.DARK);
        spark.config().setDocumentTitle("API Test Report");
        spark.config().setReportName("Rest Assured Tests");
        extent.attachReporter(spark);
    }

    @Override
    public void onTestStart(ITestResult result) {
        ExtentTest extentTest = extent.createTest(result.getMethod().getMethodName(),
            result.getMethod().getDescription());
        test.set(extentTest);
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        test.get().pass("Test passed");
    }

    @Override
    public void onTestFailure(ITestResult result) {
        test.get().fail(result.getThrowable());
    }

    @Override
    public void onFinish(ITestContext context) {
        extent.flush();
    }
}

Allure Reports

Add dependency and use annotations:

@Epic("User Management")
@Feature("User API")
public class UserApiTests extends BaseTest {

    @Test
    @Story("Create User")
    @Description("Verify user can be created successfully")
    @Severity(SeverityLevel.CRITICAL)
    public void testCreateUser() {
        CreateUserRequest user = DataGenerator.randomUser();

        Response response = sendPost(Endpoints.USERS, user);

        Allure.step("Verify status code is 201", () -> {
            response.then().statusCode(StatusCodes.CREATED);
        });

        Allure.step("Verify response contains user name", () -> {
            response.then().body("name", equalTo(user.getName()));
        });
    }
}

CI/CD Integration

Maven POM Configuration

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.2</version>
            <configuration>
                <suiteXmlFiles>
                    <suiteXmlFile>testng.xml</suiteXmlFile>
                </suiteXmlFiles>
                <systemPropertyVariables>
                    <env>${env}</env>
                </systemPropertyVariables>
            </configuration>
        </plugin>
    </plugins>
</build>

GitHub Actions Workflow

name: API Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 6 * * *'  # Daily at 6 AM

jobs:
  api-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Run API Tests
        run: mvn test -Denv=qa
        env:
          API_KEY: ${{ secrets.API_KEY }}

      - name: Upload Test Report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-report
          path: target/surefire-reports/

      - name: Publish Allure Report
        uses: simple-elf/allure-report-action@v1.7
        if: always()
        with:
          allure_results: target/allure-results

Jenkins Pipeline

pipeline {
    agent any

    parameters {
        choice(name: 'ENV', choices: ['qa', 'staging', 'prod'], description: 'Environment')
    }

    stages {
        stage('Checkout') {
            steps {
                git 'https://github.com/your-repo/api-tests.git'
            }
        }

        stage('Run Tests') {
            steps {
                sh "mvn clean test -Denv=${params.ENV}"
            }
        }

        stage('Publish Report') {
            steps {
                allure includeProperties: false,
                       jdk: '',
                       results: [[path: 'target/allure-results']]
            }
        }
    }

    post {
        always {
            junit 'target/surefire-reports/*.xml'
        }
        failure {
            mail to: 'team@example.com',
                 subject: "API Tests Failed - ${env.JOB_NAME}",
                 body: "Check: ${env.BUILD_URL}"
        }
    }
}

Best Practices

Test Data Management

// Use @DataProvider for data-driven tests
@DataProvider(name = "validUsers")
public Object[][] validUsers() {
    return new Object[][] {
        {"John", "Engineer"},
        {"Jane", "Manager"},
        {"Bob", "Developer"}
    };
}

@Test(dataProvider = "validUsers")
public void testCreateMultipleUsers(String name, String job) {
    CreateUserRequest user = CreateUserRequest.builder()
        .name(name)
        .job(job)
        .build();

    sendPost(Endpoints.USERS, user)
        .then()
        .statusCode(StatusCodes.CREATED)
        .body("name", equalTo(name));
}

Error Handling

// Custom assertions for common validations
public class ApiAssertions {

    public static void assertSuccessResponse(Response response) {
        int statusCode = response.getStatusCode();
        Assert.assertTrue(statusCode >= 200 && statusCode