I've written a lot of API tests over the years, and the ones that stuck with me weren't the ones testing some abstract "users" endpoint from a textbook. They were the ones where the data was fun enough that I actually wanted to poke at it. So instead of the usual placeholder API, this tutorial uses a small Harry Potter–themed REST API — houses, students, spells — to walk through Rest Assured from zero.
By the end you'll know how to send GET and POST requests, assert on status codes and JSON bodies, work with query parameters, and test an endpoint that requires a Bearer token. No prior Rest Assured experience needed, just some basic Java.
What you'll need
- JDK 11 or newer
- Maven (or Gradle, if you'd rather adapt the examples)
- Any IDE — IntelliJ, Eclipse, VS Code, doesn't matter
- About 30 minutes
The API we're testing against
We'll hit the Harry Potter API, a free mock REST API with no signup required. It's part of a larger playground called Fun API Playground that has a bunch of these themed sandboxes (Star Wars, Pokémon, a fake bank, that sort of thing) — useful if you want more practice material after this, but not the point of this post. It exists specifically so people can throw real HTTP requests at something without setting up their own backend first.
The base URL we'll use is:
https://funapi.dev/api/wizarding/v1
It has ten endpoints covering houses, students, and spells. Most are open reads; a few (enrolling or updating a student) need a Bearer token. That mix is actually perfect for a beginner tutorial because you get to practice both the easy case and the "wait, why is this returning 401" case.
Setting up the project
Create a Maven project and add these to your pom.xml:
<dependencies>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Rest Assured pulls in Hamcrest matchers automatically, which is what gives you that readable equalTo(...), hasItem(...) style of assertion later on. Check Maven Central if you want the latest patch versions — the numbers above were current as of writing.
Your first test: GET /houses
Let's start with the simplest possible request — listing the four houses.
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class HousesTest {
@Test
void listAllHouses() {
RestAssured.baseURI = "https://funapi.dev/api/wizarding/v1";
given()
.when()
.get("/houses")
.then()
.statusCode(200)
.body("total", equalTo(4))
.body("data.name", hasItems("Gryffindor", "Slytherin", "Hufflepuff", "Ravenclaw"));
}
}
Run that and it should pass. If you want to see what you're actually asserting against, the response looks like this:
{"total":4,"data":[{"id":1,"name":"Gryffindor","founder":"Godric Gryffindor","traits":["courage","bravery","daring"],"element":"fire","points":472},{"id":2,"name":"Slytherin","founder":"Salazar Slytherin","traits":["ambition","cunning","resourcefulness"],"element":"water","points":452},{"id":3,"name":"Hufflepuff","founder":"Helga Hufflepuff","traits":["patience","loyalty","fairness"],"element":"earth","points":428},{"id":4,"name":"Ravenclaw","founder":"Rowena Ravenclaw","traits":["wisdom","wit","creativity"],"element":"air","points":461}],"requestId":"ba44d86001332ca8"}
Notice data.name in the test — Rest Assured lets you dot into nested JSON like that without writing a single POJO. That's most of the appeal for quick API checks.
Digging deeper into the JSON body
Let's check Gryffindor specifically, by filtering the array with a Groovy-style expression Rest Assured understands out of the box:
@Test
void gryffindorHasFireElement() {
given()
.when()
.get("/houses")
.then()
.statusCode(200)
.body("find { it.name == 'Gryffindor' }.element", equalTo("fire"))
.body("find { it.name == 'Gryffindor' }.traits", hasItem("bravery"));
}
If you'd rather not memorize that Groovy syntax right away, extracting the body as a JsonPath object and reading values off it works just as well and is arguably easier to follow when you're starting out:
@Test
void gryffindorHasFireElementUsingJsonPath() {
JsonPath json = given()
.when()
.get("/houses")
.jsonPath();
int gryffindorId = json.getInt("data.find { it.name == 'Gryffindor' }.id");
assertEquals(1, gryffindorId);
}
Both approaches are common in real test suites. Pick whichever reads more naturally to you.
Working with query and path parameters
The students endpoint supports pagination and filtering by house, which is a good excuse to practice query params:
@Test
void filterStudentsByHouse() {
given()
.queryParam("houseId", 1)
.queryParam("page", 1)
.when()
.get("/students")
.then()
.statusCode(200)
.body("data.houseId", everyItem(equalTo(1)));
}
And fetching a single resource by path parameter looks like this:
@Test
void getOneStudent() {
given()
.pathParam("id", 1)
.when()
.get("/students/{id}")
.then()
.statusCode(200)
.body("name", equalTo("Evelyn Sparrow"))
.body("patronus", not(emptyOrNullString()));
}
Worth pointing out: pathParam and queryParam do different jobs, and mixing them up is a classic beginner trip-up. Path params fill in placeholders in the URL (/students/{id}), query params get appended after a ?.
Testing what happens when things go wrong
A test suite that only checks the happy path isn't testing much. Let's hit a house ID that doesn't exist:
@Test
void unknownHouseReturns404() {
given()
.pathParam("id", 99)
.when()
.get("/houses/{id}")
.then()
.statusCode(404)
.body("error", equalTo("Not Found"))
.body("message", containsString("No house with id 99"));
}
This matters more than it looks. A lot of "working" API test suites out there only ever prove the API says yes — they never prove it says no correctly, which is usually where the real bugs hide.
Testing an authenticated endpoint
Some endpoints, like enrolling a new student, require a Bearer token. This API accepts a couple of fixed test tokens (qa-admin-token and qa-viewer-token) specifically so you can practice auth flows without registering anywhere. First, let's confirm it actually rejects unauthenticated requests:
@Test
void enrollingWithoutTokenIsRejected() {
String body = "{ \"name\": \"Rowan Ashby\", \"houseId\": 1, \"year\": 1 }";
given()
.contentType("application/json")
.body(body)
.when()
.post("/students")
.then()
.statusCode(401)
.body("error", equalTo("Unauthorized"));
}
Now with the token attached:
@Test
void enrollStudentWithValidToken() {
String body = "{ \"name\": \"Rowan Ashby\", \"houseId\": 1, \"year\": 1 }";
given()
.header("Authorization", "Bearer qa-admin-token")
.contentType("application/json")
.body(body)
.when()
.post("/students")
.then()
.statusCode(201)
.body("name", equalTo("Rowan Ashby"))
.body("houseId", equalTo(1));
}
That pair of tests, rejected-then-accepted, is the pattern you'll reuse constantly once you move past tutorials and start testing real auth-protected APIs at work.
Cleaning it up with a request specification
Once you've got more than a couple of tests, repeating the base URI and headers everywhere gets old fast. Rest Assured's RequestSpecification fixes that:
import io.restassured.specification.RequestSpecification;
import io.restassured.builder.RequestSpecBuilder;
public class BaseTest {
protected static RequestSpecification spec;
static {
spec = new RequestSpecBuilder()
.setBaseUri("https://funapi.dev/api/wizarding/v1")
.setContentType("application/json")
.build();
}
}
Then any test class extending BaseTest can just do given().spec(spec).when()... instead of repeating the setup every time. It's a small change, but it's usually the first refactor every real Rest Assured suite ends up needing.
Where to go from here
You've now covered the core moves: GET and POST requests, status code and JSON body assertions, query and path parameters, error responses, and a Bearer-token-protected endpoint. That's genuinely most of what you'll use day to day.
If you want more to practice on, there's a random spell endpoint (GET /spells/random) that's a nice one for getting comfortable asserting on non-deterministic responses, and the sorting hat endpoint (POST /sorting-hat) is good for practicing POST requests that don't need a body at all. Beyond this one API, the same playground has things like pagination with cursors, OAuth flows, and endpoints that deliberately return every status code from 400 to 503, if you want to keep leveling up your API testing without switching tools.
Either way, the fastest way to actually learn Rest Assured is what you just did: stop reading, open an IDE, and start asserting on something.