Most Java API suites start the same way. Someone writes a test with HttpClient, adds a JSON parser, writes a helper to attach the auth header, and six months later there are four helper classes nobody wants to touch. The tests work. Reading them tells you nothing about what the API is supposed to do.
REST Assured exists to remove that layer. It is a Java library that gives you a readable syntax for HTTP calls and response assertions, so the test reads as the contract rather than the plumbing. This assumes you already know what API testing covers and why it sits below the UI layer.
This guide builds a working suite against a real API, then covers the failures that cost teams the most time.
What you will build
By the end you will have a REST Assured suite that authenticates, validates response bodies, reuses request configuration across tests, and fails with a message that tells you what broke. Every snippet below runs.
Prerequisites
REST Assured is a library, not a framework. It needs a JDK, a build tool and a test runner. Version pinning matters here because the 4.x and 5.x lines differ in package names for several modules.
Add the dependency and the JSON matcher module. The json-path artefact ships with the core dependency, so you do not need it separately.
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>Import the static methods. Without these two imports nothing in this guide compiles, and the error you get is a confusing "cannot find symbol" on given.
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;Step 1: The first test
REST Assured uses a given-when-then chain. given() configures the request, when() fires it, then() asserts on the response.
@Test
void returnsSingleUser() {
given()
.baseUri("https://reqres.in")
.basePath("/api")
.when()
.get("/users/2")
.then()
.statusCode(200)
.body("data.id", equalTo(2))
.body("data.email", containsString("@"));
}Run it and the test passes silently. That silence is the problem with a first test: you cannot tell whether it asserted anything. Break it deliberately by changing equalTo(2) to equalTo(99) and read the failure:
java.lang.AssertionError: 1 expectation failed.
JSON path data.id doesn't match.
Expected: <99>
Actual: <2>That message is the reason to use the library. It names the path, the expectation and the actual value without you writing any of it.
Step 2: Asserting on response bodies
The body() method takes a GPath expression, not a JsonPath expression. This trips up almost everyone arriving from a JavaScript or Python background, because the syntax looks similar and then behaves differently on collections.
For a response shaped like this:
{
"page": 2,
"data": [
{ "id": 7, "email": "[email protected]", "first_name": "Michael" },
{ "id": 8, "email": "[email protected]", "first_name": "Lindsay" }
]
}These assertions work:
.body("data.size()", equalTo(2))
.body("data[0].first_name", equalTo("Michael"))
.body("data.first_name", hasItems("Michael", "Lindsay"))
.body("data.findAll { it.id > 7 }.first_name", hasItem("Lindsay"))The last line is Groovy running inside the assertion. It is genuinely useful for filtering, and it is also where unreadable one-liners come from. Keep the filter simple or extract the response and assert in Java.
Response response = given().when().get("/users?page=2").then().extract().response();
List<String> names = response.jsonPath().getList("data.first_name");
assertThat(names).contains("Michael");Extracting is the right choice whenever you need more than two assertions on the same payload. It also gives you a real debugger breakpoint.
Step 3: Removing duplication with specifications
Copying baseUri and auth headers into forty tests is how suites rot. REST Assured has request and response specifications for exactly this.
public class ApiSpec {
public static RequestSpecification request() {
return new RequestSpecBuilder()
.setBaseUri("https://reqres.in")
.setBasePath("/api")
.setContentType(ContentType.JSON)
.addHeader("x-api-key", System.getenv("API_KEY"))
.build();
}
public static ResponseSpecification ok() {
return new ResponseSpecBuilder()
.expectStatusCode(200)
.expectContentType(ContentType.JSON)
.expectResponseTime(lessThan(3000L))
.build();
}
}Tests then carry only what makes them different:
@Test
void listsUsers() {
given().spec(ApiSpec.request())
.when().get("/users?page=2")
.then().spec(ApiSpec.ok())
.body("data", hasSize(6));
}Note the API key comes from the environment, never from the source. A credential in a test file reaches the repository history and stays there.
Step 4: Authentication
REST Assured has built-in support for the common schemes. Bearer tokens are the usual case:
given().auth().oauth2(token)For basic auth, prefer preemptive(). The non-preemptive form waits for a 401 challenge before sending credentials, which fails silently against APIs that return 403 instead.
given().auth().preemptive().basic(username, password)Fetch the token once per run rather than per test. A login call in a @BeforeAll keeps a fifty-test suite from making fifty extra round trips. Auth handling is also where API security testing overlaps with functional work: a suite that only ever tests the happy path never proves that an expired token is rejected.
@BeforeAll
static void authenticate() {
token = given()
.contentType(ContentType.JSON)
.body(Map.of("email", System.getenv("USER"), "password", System.getenv("PASS")))
.when()
.post("https://reqres.in/api/login")
.then()
.statusCode(200)
.extract().path("token");
}Step 5: Serialisation with POJOs
String payloads are fine for two fields and unmanageable for twenty. REST Assured serialises objects automatically when Jackson or Gson is on the classpath.
record CreateUser(String name, String job) {}
@Test
void createsUser() {
CreateUser payload = new CreateUser("morpheus", "leader");
given().spec(ApiSpec.request())
.body(payload)
.when()
.post("/users")
.then()
.statusCode(201)
.body("name", equalTo("morpheus"));
}If serialisation produces an empty body, the content type is almost always missing. REST Assured picks the serialiser from the content type, and with none set it does not know whether you want JSON or XML.
The errors that cost the most time
These are the failures that generate support questions, in rough order of how often they appear.
The second row is the dangerous one. A wrong GPath does not error, it returns null, and body("data.nam", equalTo(null)) is a green test asserting nothing. When a test has never failed, make it fail once on purpose.
Logging, and the one setting worth changing
By default a failing test prints the assertion but not the payload. This turns most debugging into a re-run with logging bolted on. Set it globally instead:
RestAssured.filters(new RequestLoggingFilter(LogDetail.URI),
new ResponseLoggingFilter(LogDetail.ALL));Or, less noisily, log only on failure:
.then().log().ifValidationFails(LogDetail.BODY)The second form is the better default for CI, where full logging on a large suite produces output nobody reads.
Where REST Assured is the wrong tool
Naming the limits is more useful than another feature tour.
It is a Java library, so a team with no Java skills will fight it. If your service code is TypeScript and your engineers are not JVM developers, Supertest or Playwright's request context will cost less to maintain, and maintenance dominates the total cost of an API suite.
It tests one request at a time. Contract testing between services is a different problem, and Pact solves it properly. Using REST Assured to assert both sides of a contract couples your suite to two deployments at once.
It is not a load tool. The expectResponseTime matcher asserts a single call's latency and tells you nothing about behaviour under concurrency. That is JMeter or Gatling territory.
For exploratory work against an unfamiliar API, Postman is faster. Write the exploration in Postman, then port the cases worth keeping into REST Assured where they can run in CI.
Where to start
Pick one endpoint your team breaks most often. Write three tests: the success path, one validation failure, and one authorisation failure. Put them in CI on the pull request.
Three tests running on every merge beat forty tests running when someone remembers. Once they hold, add the specification classes from step 3 before the suite reaches twenty tests, because retrofitting them later means touching every file. If the suite needs to run on every commit rather than nightly, the Postman, Newman and CI/CD pattern covers the pipeline side in more depth.
If you want a second pair of eyes on an API suite that has grown past the point of comfort, our API testing services team reviews structure, coverage and CI integration.