Your First GET and POST Request

9 min read

You've got the project skeleton and you've seen the BDD chain. Time to write code that actually goes over the wire. This lesson takes you from an empty tests/ package to two complete tests — a GET that retrieves a user and asserts on every field, and a POST that creates a user and pulls the new ID out of the response. Forty lines of Java; the patterns inside it are what every Rest Assured suite is built from.

A simple GET

We'll use JSONPlaceholder for the examples — a free fake API that returns realistic JSON. Create src/test/java/com/mycompany/apitests/tests/UserApiTest.java:

package com.mycompany.apitests.tests;
 
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
 
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
 
public class UserApiTest {
 
    @BeforeClass
    public void setup() {
        RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
    }
 
    @Test
    public void getAllUsersReturns200AndAList() {
        given()
            .header("Accept", "application/json")
        .when()
            .get("/users")
        .then()
            .statusCode(200)
            .contentType(ContentType.JSON)
            .body("size()", greaterThan(0));
    }
}

Run it from IntelliJ — green tick. Five things happened:

  1. TestNG called @BeforeClass once, setting the base URI on the static RestAssured class.
  2. given() started a new RequestSpecification, attached the Accept: application/json header.
  3. when().get("/users") resolved to GET https://jsonplaceholder.typicode.com/users, sent it, captured the response.
  4. then() ran three assertions: status code, content type, and body("size()", greaterThan(0)) — a JsonPath expression that calls size() on the root array and compares with a Hamcrest matcher.
  5. TestNG reported success.

GET with a path parameter

Most real APIs identify a resource by ID — /users/{userId}. Rest Assured's pathParam() templates the value into the URL safely (encoding any special characters):

@Test
public void getUserByIdReturnsAlice() {
    given()
        .pathParam("userId", 1)
    .when()
        .get("/users/{userId}")
    .then()
        .statusCode(200)
        .body("name", equalTo("Leanne Graham"))
        .body("email", equalTo("Sincere@april.biz"))
        .body("address.city", equalTo("Gwenborough"));
}

Notice three things. The placeholder {userId} in the URL is matched by the pathParam("userId", 1) call. The dotted JSON path (address.city) walks into nested fields. And every .body() assertion is independent — failing one fails the test with a clear message identifying the field.

A complete POST

Creating a resource needs a body. The simplest way to write a JSON body in modern Java is a text block (Java 13+):

@Test
public void createUserReturns201AndAnId() {
    String requestBody = """
        {
            "name": "Bob Smith",
            "email": "bob@test.com",
            "username": "bobsmith",
            "phone": "555-1234"
        }
        """;
 
    given()
        .contentType(ContentType.JSON)
        .body(requestBody)
    .when()
        .post("/users")
    .then()
        .statusCode(201)
        .contentType(ContentType.JSON)
        .body("id", notNullValue())
        .body("name", equalTo("Bob Smith"))
        .body("email", equalTo("bob@test.com"));
}

Three new things compared to the GET:

  • .contentType(ContentType.JSON) — sets the Content-Type: application/json header so the server knows how to parse the body. Forget this and many APIs will reject the request as unsupported media type.
  • .body(requestBody) — attaches the body to the request. Strings, Maps, POJOs, and File objects are all accepted (Lessons 3 and 4 of the next chapter cover each).
  • .statusCode(201) — POST that creates a resource conventionally returns 201 Created, not 200. (You learned this in the API Testing Masterclass lesson on status codes — now you're enforcing it programmatically.)

JSONPlaceholder is a fake — it doesn't actually persist anything — but it returns a realistic 201 with a generated id, which is exactly what your assertions need.

Extracting a value from the response

Real test flows chain: create a user, then PATCH them, then DELETE them. To do that you need the new ID from the create response. extract() breaks the chain and returns a value:

@Test
public void createThenDeleteUser() {
    String requestBody = """
        { "name": "Temp User", "email": "temp@test.com" }
        """;
 
    int newUserId = given()
        .contentType(ContentType.JSON)
        .body(requestBody)
    .when()
        .post("/users")
    .then()
        .statusCode(201)
        .extract()
        .path("id");   // pulls the "id" field out of the response body
 
    System.out.println("Created user ID: " + newUserId);
 
    given()
        .pathParam("userId", newUserId)
    .when()
        .delete("/users/{userId}")
    .then()
        .statusCode(200);
}

extract().path("id") evaluates a JsonPath expression against the response body. If the field were nested, you'd write .path("user.id") or .path("data.user.id") — same dotted-path notation as in the assertion body(...) calls.

When you need the whole Response

Sometimes you want everything: status, headers, body, time. Skip then() and capture the Response:

import io.restassured.response.Response;
 
@Test
public void inspectFullResponse() {
    Response response = given().when().get("/users/1");
 
    int statusCode = response.getStatusCode();
    String body = response.getBody().asString();
    String contentType = response.getHeader("Content-Type");
    long timeMs = response.getTime();
 
    System.out.println("Status: " + statusCode);
    System.out.println("Time: " + timeMs + " ms");
    System.out.println("Body: " + body);
}

Console output:

Status: 200
Time: 124 ms
Body: {"id":1,"name":"Leanne Graham","username":"Bret",...}

Use this form sparingly — for the typical assertion case, the then() chain is more readable. The Response object earns its keep when you need to log, branch, or save something to disk.

GET vs POST at a glance

The two verbs cover the common case. PUT and PATCH (update) and DELETE (remove) follow the same shape — body for PUT/PATCH, no body for DELETE — and we'll write tests for each in later chapters.

Where this fits in a real suite

Notice what we haven't done yet: no shared RequestSpecification, no auth, no POJO models, no data-driven cases. Each of those gets its own chapter. What you've learned here is the irreducible core — given().when().<verb>().then().<assertions> plus optional extract() — and that core handles maybe 60% of the tests in a real suite. The other 40% is composition on top of it.

Bookmark the Rest Assured cheat sheet on qa.codes for the full DSL surface. Most of what you'll type in the rest of this course is on one page.

⚠️ Common mistakes

  • Forgetting .contentType(ContentType.JSON) on POST/PUT/PATCH. Without it, Rest Assured doesn't set the Content-Type header and many servers reject the body as unsupported media type — or worse, accept it and silently misparse. Always set the content type when you have a body.
  • Mixing body(...) for sending and asserting. In the request block (given()), body(payload) attaches the request body. In the assertion block (then()), body("path", matcher) asserts on the response. They're different methods on different specs that happen to share a name. Putting one in the wrong place either fails to compile or fails with a confusing error.
  • Asserting only on status code. 200 OK with the wrong body is still a bug — usually a worse bug because tests look green. Always pair the status with at least one body field assertion. The cost is one extra line; the payoff is real coverage.

🎯 Practice task

Wire a complete create-read-delete flow against JSONPlaceholder. 30–40 minutes.

  1. In your Maven project, create tests/UserApiTest.java. Set RestAssured.baseURI = "https://jsonplaceholder.typicode.com" in @BeforeClass.
  2. Write getAllUsersReturns200AndAList() — copy from this lesson, run it, confirm green.
  3. Write getUserByIdReturnsAlice() using the path parameter pattern. Assert on at least three fields of user 1 (name, email, address.city). Run it green.
  4. Write createUserReturns201AndAnId() — POST a new user with a text-block JSON body. Assert status is 201 and the response has an id that is not null and a name matching what you sent.
  5. Combine into createThenDeleteUser() — POST, extract the ID, then DELETE the user by that ID. Print the ID before the DELETE so you can see it in the test output. Both calls should be green.
  6. Make a PUT. Add updateUserById() — PUT /users/1 with a full user body and assert 200. Note how the server returns the updated resource — assert the changed fields actually changed in the response.
  7. Stretch: capture the full Response for one of the GETs into a variable. Print the response time. Add an assertion that the response time is under 2000 ms — Rest Assured supports .time(lessThan(2000L)) directly inside then(). Make it fail by setting the limit to 1 ms; read the failure message; restore.

Next lesson: response validation in depth — every Hamcrest matcher worth knowing, JsonPath expressions for nested fields and arrays, and how to chain a dozen assertions in one fluent block.

// tip to track lessons you complete and pick up where you left off across devices.