# API Testing with REST Assured — GET, POST, Auth Tokens, Status Codes and JSON Checks

Source: https://skills101.in/learn/test-automation/rest-assured-api-testing-java — Skills101 free tutorial (Software Testing & Automation). Updated 2026-11-09.

UI tests check what users see; **API tests** check the business rules underneath — faster, more stable, and possible before the UI even exists. **REST Assured** is the standard Java library for them.

Our API tests run against the **Course API** we build in the [Spring Boot track](https://skills101.in/learn/spring-boot/spring-boot-rest-api): courses, login with JWT tokens, roles.

![An API test has three parts: given sets headers, token, body and parameters; when sends the request; then checks status, headers and body fields](https://skills101.in/learn-img/test-automation/rest-assured.svg)

## 1. HTTP in two minutes

A request has a **method** (GET read, POST create, PUT update, DELETE remove), a **URL**, **headers** (like `Content-Type: application/json`, `Authorization: Bearer …`) and maybe a **JSON body**. The response has a **status code** (200 OK, 201 Created, 400 bad input, 401 not logged in, 403 not allowed, 404 not found, 409 conflict, 500 server error), headers and a body.

## 2. The tests

```java
// File: src/test/java/com/skills101/qa/api/CourseApiTest.java
package com.skills101.qa.api;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.notNullValue;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

/**
 * API tests against the running Course API (examples/spring-boot/course-api).
 * Start it first, or point the tests elsewhere with -DapiUrl=https://staging.example.com
 */
public class CourseApiTest {

    @BeforeClass(alwaysRun = true)
    public void setUp() {
        RestAssured.baseURI = System.getProperty("apiUrl", "http://localhost:8081");
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
    }

    private String tokenFor(String email, String password) {
        return given()
                .contentType(ContentType.JSON)
                .body("{\"email\":\"" + email + "\",\"password\":\"" + password + "\"}")
            .when()
                .post("/auth/token")
            .then()
                .statusCode(200)
                .body("type", equalTo("Bearer"))
                .extract().path("token");
    }

    @Test(groups = "smoke")
    public void listCourses() {
        given()
        .when()
            .get("/api/courses")
        .then()
            .statusCode(200)
            .contentType(ContentType.JSON)
            .body("title", hasItems("Java Full Stack", "DSA in Python"))
            .body("fee", everyItem(greaterThan(0)));
    }

    @Test
    public void getOneCourse() {
        given()
            .pathParam("id", 1)
        .when()
            .get("/api/courses/{id}")
        .then()
            .statusCode(200)
            .body("title", equalTo("Java Full Stack"))
            .body("fee", equalTo(4999));
    }

    @Test
    public void unknownCourseIs404WithProblemDetail() {
        given()
        .when()
            .get("/api/courses/9999")
        .then()
            .statusCode(404)
            .contentType("application/problem+json")
            .body("detail", equalTo("Course 9999 not found"));
    }

    @Test
    public void createWithoutTokenIs401() {
        given()
            .contentType(ContentType.JSON)
            .body("{\"title\":\"Selenium Basics\",\"fee\":999,\"seats\":20}")
        .when()
            .post("/api/courses")
        .then()
            .statusCode(401);
    }

    @Test
    public void adminCreatesCourseAndCanReadItBack() {
        String token = tokenFor("admin@skills101.in", "admin123");

        int id = given()
                .auth().oauth2(token)                       // adds "Authorization: Bearer <token>"
                .contentType(ContentType.JSON)
                .body("{\"title\":\"API Testing with REST Assured\",\"fee\":1499,\"seats\":25}")
            .when()
                .post("/api/courses")
            .then()
                .statusCode(201)
                .header("Location", notNullValue())
                .body("title", equalTo("API Testing with REST Assured"))
                .extract().path("id");

        given().pathParam("id", id)
            .when().get("/api/courses/{id}")
            .then().statusCode(200).body("seats", equalTo(25));
    }

    @Test
    public void invalidCourseIs400WithFieldErrors() {
        String token = tokenFor("admin@skills101.in", "admin123");
        given()
            .auth().oauth2(token)
            .contentType(ContentType.JSON)
            .body("{\"title\":\"\",\"fee\":-1,\"seats\":0}")
        .when()
            .post("/api/courses")
        .then()
            .statusCode(400)
            .body("errors.title", equalTo("must not be blank"))
            .body("errors.seats", equalTo("must be greater than or equal to 1"));
    }

    @Test
    public void studentCannotCreateCourses() {
        String token = tokenFor("asha@skills101.in", "asha123");
        given()
            .auth().oauth2(token)
            .contentType(ContentType.JSON)
            .body("{\"title\":\"Nope\",\"fee\":1,\"seats\":1}")
        .when()
            .post("/api/courses")
        .then()
            .statusCode(403);
    }
}
```

All 7 tests pass against the running API. What they show:

- **given / when / then** reads like a sentence: given these headers and body, when I POST, then expect 201.
- **JSON paths**: `body("title", equalTo(...))` reads a field; on a list, `"title"` gives all titles, so `hasItems(...)` and `everyItem(...)` check collections. Nested fields: `"errors.title"`.
- **Auth**: `tokenFor()` logs in, `extract().path("token")` pulls the token out, and `.auth().oauth2(token)` sends it as a Bearer header.
- **Chaining**: create a course, extract its `id`, then GET it back.
- **Negative tests** matter as much as happy paths: 401 without a token, 403 for a student, 400 for invalid data, 404 for unknown ids.
- `enableLoggingOfRequestAndResponseIfValidationFails()` prints the full request and response only when a test fails — great for debugging.
- The base URL comes from `-DapiUrl`, so the same tests run against local, staging or production-like environments.

## 3. More you'll use at work

- **JSON schema validation** (`matchesJsonSchemaInClasspath("course-schema.json")`) — checks the shape of every field at once.
- **POJOs** instead of JSON strings: `.body(new CourseRequest("Git", 0, 50))`, and `.extract().as(CourseResponse.class)`.
- **Request/response specifications** to share base URI, headers and auth across tests.
- **Response time**: `.time(lessThan(2000L))`.

## Common mistakes

- Only testing happy paths.
- Hard-coding tokens that expire — log in during the test.
- Asserting the whole JSON as a string (breaks when field order changes).
- Tests that depend on data created by other tests; create what you need, ideally with unique names.

## Quick recap

- given (headers, auth, body) → when (method + path) → then (status, headers, body).
- JSON paths + Hamcrest matchers check fields and lists.
- Log in during the test, extract the token, send it as Bearer.
- Cover negative cases: 400, 401, 403, 404.


Practice quiz and free certificate: https://skills101.in/learn/test-automation/rest-assured-api-testing-java