Sometimes you need to send REST requests with query parameters from a Spring Boot application context. Fortunately, you can easily do that with WebClient.

It's not very intuitive, though. You might have to do some research on how to make it happen.

And maybe that research brought you here.

So without further delay, let's get started.

The Business Requirements

Your boss Smithers walks into your office wearing a Kenny Chesney t-shirt. He takes a seat.

"It's about that CRM app you're working on," he says. "We need to start making calls between microservices."

He pauses, awkwardly waiting for you to say something.

"Well anyway, you need to add a service-layer call from the customer service application to the CRM service application. Sometimes, people other than sales reps might need to see sales activity."

Smithers sighs.

"I'm sure you can handle that," he says as he gets up and walks out of your office.

He's clearly disappointed that you didn't say anything during the whole meeting.

Technical Stuff

Fortunately, the CRM service already exposes a REST endpoint that allows for searching activities. That endpoint is intuitively called /activities/search

Clients who use the endpoint are expected to include search criteria as request parameters. For example:

/activities/search?contactId=6014199147692f2a4194ff9a&orderBy=startDate&orderType=DESC

That will return all activities for the contact specified by contact ID in descending order of start date.

You can easily make that kind of call with a WebClient instance.

By the way, if you want to see the data that I'm currently working with, just take a look at the JSON dump at this link.

Web Work

Start by instantiating a WebClient. You can do that with code that looks like this:

WebClient crmClient = WebClient
                         .builder()
                         .baseUrl(BASE_URL)
                         .build();

Here, BASE_URL is the base URL of wherever the CRM service is located. For example, it might be http://localhost:8080 if you're just working in a local environment.

By the way, if you'd like a beginner's guide, feel free to check out my other article on WebClient.

It's a great idea to instantiate the WebClient up front because you might use it for several requests to different endpoints.

Speaking of using the WebClient instance, here's how you do that:

String contactId = "6014199147692f2a4194ff9a";

List<Activity> activities = crmClient
                            .get()
                            .uri(uriBuilder -> uriBuilder
                                    .path("/activities/search")
                                    .queryParam("contactId", contactId)
                                    .queryParam("orderBy", "startDate")
                                    .queryParam("orderType", "DESC")
                                    .build())
                            .header(HttpHeaders.AUTHORIZATION, bearerToken)
                            .retrieve()
                            .bodyToFlux(Activity.class)
                            .collectList()
                            .block();

try {
    ObjectMapper objectMapper = new ObjectMapper();
    System.err.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(activities));
} catch (Exception e) {
    e.printStackTrace();
}

The code above uses a GET request to retrieve the activities. You can see that clearly with the get() method.

After that method, it specifies the URI. It does that with a Function that uses UriBuilder.

But what the heck is UriBuilder? It's an interface that defines convenience methods so that you can easily build a URI.

You can see a couple of those convenience methods above. One of them, path(), specifies the full path following the base path that you defined in BASE_URL.

Following that, you'll see three queryParam() methods. That's where you put your query parameters. The first parameter in queryParam() specifies the name of the query parameter. The second parameter specifies its value.

If you look closely at the code above, you'll see that it builds exactly the URI you saw in the previous section.

That lambda expression, by the way, invokes the apply() method in a Function. That Function uses UriBuilder to create the URI itself.

Once the URI is created, it's time to deal with security. That's why the code uses header() to set the "Authorization" header to the bearer token.

The retrieve() method is the part that actually performs the HTTP request. Just keep in mind that this code operates within a reactive framework so nothing actually happens until somebody subscribes to the stream.

Once the response comes back, the bodyToFlux() method converts it to a Flux object. It uses Flux instead of Mono because it's expecting an array of results rather than a single result.

After that, the code coverts the Flux to a List of Activity objects. But that List is wrapped in a Mono

Yeah. This API could probably be a little cleaner.

Finally, the block() method subscribes to the stream to make the whole REST request thing actually happen.

But What Happens?

So what kind of output will you get from that?

Take a look at the bottom of the code block above. You'll see that it's got a try/catch block. Inside there it translates the response from the downstream service into a JSON array and then prints it out in nice red characters on the screen.

If you run the code above with the dataset I referenced earlier, you'll get this result:

[ {
  "id" : "601fe0718fc6df7eddcf3ae7",
  "type" : {
    "id" : "6016a960ca8c08019b4dfcbe",
    "name" : "Appointment",
    "icon" : "calendar_today"
  },
  "title" : "Lunch",
  "outcome" : null,
  "notes" : "Good conversation about her staffing issues and requirements for her clients.",
  "location" : "Banana Joe's",
  "startDate" : 1611852300000,
  "endDate" : 1611855900000,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
}, {
  "id" : "601fdf948fc6df7eddcf3ae6",
  "type" : {
    "id" : "6016a960ca8c08019b4dfcbc",
    "name" : "Phone Call",
    "icon" : "phone"
  },
  "title" : "Good conversation about Java Enterprise",
  "outcome" : null,
  "notes" : "She said that she's working with the HR team to get everything up and running.",
  "location" : null,
  "startDate" : 1611067500000,
  "endDate" : null,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
}, {
  "id" : "601fde938fc6df7eddcf3ae5",
  "type" : {
    "id" : "6016a64c33ffe20d90fa8232",
    "name" : "Email",
    "icon" : "email"
  },
  "title" : "Sent another email just to follow up",
  "outcome" : {
    "id" : "6016a407ae0e817a115fd069",
    "name" : "Did Not Respond"
  },
  "notes" : "This was a follow-up to the first email.",
  "location" : null,
  "startDate" : 1610713800000,
  "endDate" : null,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
}, {
  "id" : "601fdc208fc6df7eddcf3ae4",
  "type" : {
    "id" : "6016a64c33ffe20d90fa8232",
    "name" : "Email",
    "icon" : "email"
  },
  "title" : "Email Followup from lunch",
  "outcome" : {
    "id" : "6016a407ae0e817a115fd069",
    "name" : "Did Not Respond"
  },
  "notes" : null,
  "location" : null,
  "startDate" : 1610627400000,
  "endDate" : null,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
}, {
  "id" : "601fdb8c8fc6df7eddcf3ae3",
  "type" : {
    "id" : "6016a960ca8c08019b4dfcbe",
    "name" : "Appointment",
    "icon" : "calendar_today"
  },
  "title" : "Lunch",
  "outcome" : null,
  "notes" : "Good conversation about our existing clients. She seemed very interested.",
  "location" : "That Italian Joynt",
  "startDate" : 1610558100000,
  "endDate" : 1610563500000,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
}, {
  "id" : "601fdafc8fc6df7eddcf3ae2",
  "type" : {
    "id" : "6016a960ca8c08019b4dfcbd",
    "name" : "Chat",
    "icon" : "chat"
  },
  "title" : "Chatted with her via Skype",
  "outcome" : {
    "id" : "6016a408ae0e817a115fd06a",
    "name" : "Interested"
  },
  "notes" : "Talked about our history of delivering outstanding Java Enterprise software.",
  "location" : null,
  "startDate" : 1610367300000,
  "endDate" : null,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
}, {
  "id" : "601fd76fda249c7531fd28f3",
  "type" : {
    "id" : "6016a64c33ffe20d90fa8232",
    "name" : "Email",
    "icon" : "email"
  },
  "title" : "Emailed me asking about REST development",
  "outcome" : {
    "id" : "6016a408ae0e817a115fd06c",
    "name" : "Appointment Scheduled"
  },
  "notes" : "She's looking to a legacy project into a solution with REST and MongoDB.",
  "location" : null,
  "startDate" : 1609855200000,
  "endDate" : null,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
} ]

And that's what you'd expect to see because the contact ID you provided above is, in fact, Joy Yeezu's contact ID. And those are her activities in descending order by start date.

In other words: you have achieved success. Congratulations.

Wrapping It Up

The lesson is not over. Take what you've learned here and apply it to your own WebClient requirements.

Use a different endpoint. Specify other request parameters. Request a single element instead of an array.

But whatever you do, just make sure you have fun!

Photo by Mary Taylor from Pexels