Home / Blog / REST Assured Tutorial: Build a Complete API Test Framework in Java

API Testing

REST Assured Tutorial: Build a Complete API Test Framework in Java

QA Knowledge Hub·2026-04-08·7 min read

REST Assured is the standard Java library for API test automation. It provides a fluent, readable DSL (Domain-Specific Language) for making HTTP requests and validating responses. If you are targeting SDET roles that use Java, REST Assured is a non-negotiable skill.

Setup: Maven Dependencies

Add to your pom.xml:

<dependencies>
    <!-- REST Assured -->
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <version>5.4.0</version>
        <scope>test</scope>
    </dependency>

    <!-- JSON Schema Validation -->
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>json-schema-validator</artifactId>
        <version>5.4.0</version>
        <scope>test</scope>
    </dependency>

    <!-- TestNG -->
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>7.9.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Jackson for JSON serialisation -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.16.1</version>
    </dependency>
</dependencies>

Your First REST Assured Test

import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.testng.annotations.Test;

import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

public class FirstApiTest {

    @Test
    public void testGetUser() {
        given()
            .baseUri("https://jsonplaceholder.typicode.com")
        .when()
            .get("/users/1")
        .then()
            .statusCode(200)
            .body("id", equalTo(1))
            .body("name", notNullValue())
            .body("email", containsString("@"));
    }
}

This is REST Assured's fluent syntax: given() sets up the request, when() sends it, then() validates the response.

The given/when/then Pattern

given()
    // Request setup
    .baseUri("https://api.example.com")
    .header("Authorization", "Bearer " + token)
    .header("Content-Type", "application/json")
    .queryParam("page", 1)
    .queryParam("size", 10)
.when()
    // HTTP method and path
    .get("/users")
.then()
    // Response assertions
    .statusCode(200)
    .time(lessThan(2000L))  // Response under 2 seconds
    .body("data.size()", greaterThan(0))
    .body("data[0].id", notNullValue());

Base Configuration — API Test Base Class

// src/test/java/com/qaknowledgehub/base/ApiTestBase.java
package com.qaknowledgehub.base;

import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.filter.log.RequestLoggingFilter;
import io.restassured.filter.log.ResponseLoggingFilter;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import org.testng.annotations.BeforeClass;

import static org.hamcrest.Matchers.lessThan;

public class ApiTestBase {

    protected static RequestSpecification requestSpec;
    protected static ResponseSpecification responseSpec;

    @BeforeClass
    public static void setup() {
        RestAssured.baseURI = "https://jsonplaceholder.typicode.com";

        requestSpec = new RequestSpecBuilder()
            .setContentType("application/json")
            .addFilter(new RequestLoggingFilter())   // Log all requests
            .addFilter(new ResponseLoggingFilter())  // Log all responses
            .build();

        responseSpec = new ResponseSpecBuilder()
            .expectResponseTime(lessThan(3000L))     // All responses < 3s
            .build();
    }
}

GET Requests — Fetching Data

// src/test/java/com/qaknowledgehub/tests/GetUserTests.java
package com.qaknowledgehub.tests;

import com.qaknowledgehub.base.ApiTestBase;
import io.restassured.response.Response;
import org.testng.Assert;
import org.testng.annotations.Test;

import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

public class GetUserTests extends ApiTestBase {

    @Test(description = "GET /users/1 returns correct user data")
    public void testGetUserById() {
        given()
            .spec(requestSpec)
        .when()
            .get("/users/1")
        .then()
            .spec(responseSpec)
            .statusCode(200)
            .body("id", equalTo(1))
            .body("name", equalTo("Leanne Graham"))
            .body("email", equalTo("Sincere@april.biz"));
    }

    @Test(description = "GET /users returns a list of 10 users")
    public void testGetAllUsers() {
        given()
            .spec(requestSpec)
        .when()
            .get("/users")
        .then()
            .statusCode(200)
            .body("size()", equalTo(10))
            .body("[0].id", notNullValue())
            .body("[0].name", notNullValue());
    }

    @Test(description = "GET /users/9999 returns 404 for non-existent user")
    public void testGetNonExistentUser() {
        given()
            .spec(requestSpec)
        .when()
            .get("/users/9999")
        .then()
            .statusCode(404);
    }

    @Test(description = "Extract and use response body values")
    public void testExtractResponseValues() {
        Response response = given()
            .spec(requestSpec)
        .when()
            .get("/users/1")
        .then()
            .statusCode(200)
            .extract()
            .response();

        String name = response.jsonPath().getString("name");
        String email = response.jsonPath().getString("email");
        int userId = response.jsonPath().getInt("id");

        Assert.assertEquals(userId, 1);
        Assert.assertNotNull(name);
        Assert.assertTrue(email.contains("@"), "Email should contain @");
    }
}

POST Requests — Creating Resources

Using HashMap

@Test(description = "POST /posts creates a new post")
public void testCreatePost() {
    Map<String, Object> payload = new HashMap<>();
    payload.put("title", "My Test Post");
    payload.put("body", "This is the post body.");
    payload.put("userId", 1);

    given()
        .spec(requestSpec)
        .body(payload)
    .when()
        .post("/posts")
    .then()
        .statusCode(201)
        .body("title", equalTo("My Test Post"))
        .body("id", notNullValue());
}

Using POJO (Plain Old Java Object)

// src/main/java/com/qaknowledgehub/models/Post.java
public class Post {
    private String title;
    private String body;
    private int userId;

    public Post(String title, String body, int userId) {
        this.title = title;
        this.body = body;
        this.userId = userId;
    }

    // Getters (required for Jackson serialisation)
    public String getTitle() { return title; }
    public String getBody() { return body; }
    public int getUserId() { return userId; }
}
@Test
public void testCreatePostWithPojo() {
    Post newPost = new Post("Interview Prep Guide", "Complete guide to QA interviews", 1);

    given()
        .spec(requestSpec)
        .body(newPost)
    .when()
        .post("/posts")
    .then()
        .statusCode(201)
        .body("title", equalTo("Interview Prep Guide"))
        .body("userId", equalTo(1));
}

PUT and PATCH Requests

@Test(description = "PUT /posts/1 replaces the entire post")
public void testUpdatePostWithPut() {
    Map<String, Object> updatedPost = new HashMap<>();
    updatedPost.put("id", 1);
    updatedPost.put("title", "Updated Title");
    updatedPost.put("body", "Updated body content.");
    updatedPost.put("userId", 1);

    given()
        .spec(requestSpec)
        .body(updatedPost)
    .when()
        .put("/posts/1")
    .then()
        .statusCode(200)
        .body("title", equalTo("Updated Title"));
}

@Test(description = "PATCH /posts/1 updates only specified fields")
public void testPartialUpdateWithPatch() {
    Map<String, String> patchPayload = new HashMap<>();
    patchPayload.put("title", "Only the Title Changed");

    given()
        .spec(requestSpec)
        .body(patchPayload)
    .when()
        .patch("/posts/1")
    .then()
        .statusCode(200)
        .body("title", equalTo("Only the Title Changed"));
}

DELETE Requests

@Test(description = "DELETE /posts/1 removes the post")
public void testDeletePost() {
    given()
        .spec(requestSpec)
    .when()
        .delete("/posts/1")
    .then()
        .statusCode(200);  // JSONPlaceholder returns 200 for DELETE
}

Authentication

Bearer Token

String token = "eyJhbGciOiJIUzI1NiJ9...";  // From login response

given()
    .header("Authorization", "Bearer " + token)
    .spec(requestSpec)
.when()
    .get("/profile")
.then()
    .statusCode(200);

Basic Auth

given()
    .auth().basic("username", "password")
    .spec(requestSpec)
.when()
    .get("/admin/users")
.then()
    .statusCode(200);

OAuth 2.0 Flow (Login → Use Token)

@Test
public void testAuthenticatedEndpoint() {
    // Step 1: Login and extract token
    String token = given()
        .spec(requestSpec)
        .body(Map.of("username", "user1", "password", "pass123"))
    .when()
        .post("/auth/login")
    .then()
        .statusCode(200)
        .extract()
        .jsonPath()
        .getString("token");

    Assert.assertNotNull(token, "Token should not be null");

    // Step 2: Use token for authenticated request
    given()
        .spec(requestSpec)
        .header("Authorization", "Bearer " + token)
    .when()
        .get("/profile")
    .then()
        .statusCode(200)
        .body("username", equalTo("user1"));
}

Query Parameters and Path Parameters

// Query parameters: /users?page=1&size=20
given()
    .spec(requestSpec)
    .queryParam("page", 1)
    .queryParam("size", 20)
.when()
    .get("/users")
.then()
    .statusCode(200);

// Path parameters: /users/{userId}/orders/{orderId}
given()
    .spec(requestSpec)
    .pathParam("userId", 42)
    .pathParam("orderId", 1001)
.when()
    .get("/users/{userId}/orders/{orderId}")
.then()
    .statusCode(200);

JSON Schema Validation

Schema validation verifies the response structure (field names, types, required fields) — not just field values.

Create src/test/resources/schemas/user_schema.json:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["id", "name", "email", "phone"],
  "properties": {
    "id": { "type": "integer" },
    "name": { "type": "string", "minLength": 1 },
    "email": { "type": "string", "format": "email" },
    "phone": { "type": "string" }
  }
}
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;

@Test
public void testUserResponseMatchesSchema() {
    given()
        .spec(requestSpec)
    .when()
        .get("/users/1")
    .then()
        .statusCode(200)
        .body(matchesJsonSchemaInClasspath("schemas/user_schema.json"));
}

This catches contract breaks — if a field is renamed or a required field is removed, the schema validation fails immediately.

Data-Driven Testing with TestNG DataProvider

@DataProvider(name = "userIds")
public Object[][] provideUserIds() {
    return new Object[][] {
        {1, 200},
        {5, 200},
        {10, 200},
        {9999, 404}
    };
}

@Test(dataProvider = "userIds")
public void testGetUserWithVariousIds(int userId, int expectedStatus) {
    given()
        .spec(requestSpec)
    .when()
        .get("/users/" + userId)
    .then()
        .statusCode(expectedStatus);
}

Complete Test Example: CRUD Operations Chain

public class CrudOperationsTest extends ApiTestBase {

    private static int createdPostId;

    @Test(priority = 1, description = "Create a new post")
    public void testCreatePost() {
        Map<String, Object> payload = Map.of(
            "title", "CRUD Test Post",
            "body", "Testing create operation",
            "userId", 1
        );

        createdPostId = given()
            .spec(requestSpec)
            .body(payload)
        .when()
            .post("/posts")
        .then()
            .statusCode(201)
            .body("title", equalTo("CRUD Test Post"))
            .extract()
            .jsonPath()
            .getInt("id");

        Assert.assertTrue(createdPostId > 0, "Post ID should be positive");
    }

    @Test(priority = 2, description = "Read the created post", dependsOnMethods = "testCreatePost")
    public void testReadCreatedPost() {
        // JSONPlaceholder doesn't persist, so we read an existing post
        given()
            .spec(requestSpec)
        .when()
            .get("/posts/1")
        .then()
            .statusCode(200)
            .body("id", equalTo(1));
    }

    @Test(priority = 3, description = "Update the post")
    public void testUpdatePost() {
        given()
            .spec(requestSpec)
            .body(Map.of("title", "Updated Post Title"))
        .when()
            .patch("/posts/1")
        .then()
            .statusCode(200)
            .body("title", equalTo("Updated Post Title"));
    }

    @Test(priority = 4, description = "Delete the post")
    public void testDeletePost() {
        given()
            .spec(requestSpec)
        .when()
            .delete("/posts/1")
        .then()
            .statusCode(200);
    }
}

Common REST Assured Hamcrest Matchers

// Equality
.body("status", equalTo("success"))
.body("count", equalTo(10))

// Null checks
.body("data", notNullValue())
.body("error", nullValue())

// String matchers
.body("message", containsString("successfully"))
.body("email", endsWith("@example.com"))
.body("url", startsWith("https://"))

// Numeric comparisons
.body("count", greaterThan(0))
.body("price", lessThanOrEqualTo(999.99f))

// Collection matchers
.body("tags", hasSize(3))
.body("tags", hasItems("qa", "automation"))
.body("ids", everyItem(greaterThan(0)))

// Type checking
.body("id", instanceOf(Integer.class))

Summary

You now have a complete REST Assured framework with:

  • Base request/response specifications (no duplication across tests)
  • GET, POST, PUT, PATCH, DELETE test coverage
  • Authentication patterns (Bearer token, Basic Auth)
  • JSON Schema validation for contract testing
  • Data-driven tests with TestNG @DataProvider
  • CRUD operation chaining

This framework structure is exactly what SDET interviewers expect to see in a portfolio project. Push it to GitHub, add 20+ tests, and you have a strong interview talking point.

Recommended Resource

API Testing Notes Pack

Structured API testing notes for interview and practical project execution.

799Get This Guide →

Related Posts

📝
Interview Prep
Apr 2026·12 min read

35 Playwright Interview Questions (With Answers)

Playwright interview questions and answers for QA and SDET roles — covering setup, locators, waits, fixtures, API testing, and debugging.

Read article →
📝
Selenium
Apr 2026·8 min read

Page Object Model in Selenium: Design and Best Practices

Learn the Page Object Model design pattern for Selenium — why it exists, how to implement it correctly in Java, and the mistakes to avoid.

Read article →
📝
Interview Prep
Apr 2026·11 min read

60 Selenium Interview Questions for Java Developers

The most asked Selenium + Java interview questions with complete answers — covering WebDriver setup, locators, waits, POM, TestNG, and framework design.

Read article →