Groovy REST API consumption with 10+ examples. Fetch JSON with URL.text, HttpURLConnection, POST requests, headers, and error handling on Groovy 5.x.
“The best thing about Groovy’s HTTP support is that you can hit an API and parse the response in two lines of code. Try doing that in plain Java.”
Roy Fielding, REST Dissertation
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate | Reading Time: 24 minutes
Modern applications live and breathe APIs. Pulling data from a third-party service, automating CI/CD pipelines, building integration scripts – all of these require HTTP requests and JSON response parsing. Groovy makes this ridiculously easy – you can fetch and parse a JSON API in literally two lines of code.
This Groovy REST API guide covers everything from the simplest URL.text approach to full HttpURLConnection control. We’ll walk through GET and POST requests, custom headers, JSON request bodies, error handling, timeouts, authentication, and real-world patterns – all with tested examples.
Make sure you’re comfortable with Groovy JSON Parsing with JsonSlurper and Groovy JSON Output with JsonBuilder before diving in – we’ll use both extensively.
Table of Contents
What Is REST API Consumption in Groovy?
REST API consumption means making HTTP requests to external services and processing their responses. In Groovy, you can do this using built-in JDK classes – no third-party HTTP libraries are required for basic use cases.
According to the Groovy Development Kit documentation, Groovy adds convenience methods to Java’s URL and I/O classes that make HTTP operations much simpler.
Key Points:
URL.text– one-line GET request that returns the response body as a stringHttpURLConnection– full control over method, headers, body, and response codesJsonSlurper– parse JSON responses directly into Groovy maps and listsJsonOutput– generate JSON request bodies from maps and objects- No external libraries needed for basic HTTP – everything uses
java.net - For advanced needs (connection pooling, async), consider Apache HttpClient or OkHttp
Why Use Groovy for API Calls?
- Minimal code – a GET + JSON parse is two lines:
new JsonSlurper().parse(new URL(...)) - No dependencies – uses JDK’s
java.net.URLandHttpURLConnection - Dynamic JSON handling – no need for POJOs or type mapping
- Great for scripting – ideal for Jenkins pipelines, automation scripts, and quick integrations
- Collection methods – filter, transform, and aggregate API responses with
findAll(),collect(),groupBy()
Basic Syntax
There are two main approaches to making HTTP requests in Groovy:
Two Approaches to HTTP in Groovy
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
// APPROACH 1: URL.text - simplest possible GET
// def json = new JsonSlurper().parseText(new URL('https://api.example.com/data').text)
// APPROACH 2: HttpURLConnection - full control
// def conn = new URL('https://api.example.com/data').openConnection() as HttpURLConnection
// conn.requestMethod = 'GET'
// conn.setRequestProperty('Accept', 'application/json')
// def json = new JsonSlurper().parseText(conn.inputStream.text)
// conn.disconnect()
Approach 1 is perfect for quick scripts. Approach 2 gives you control over method, headers, timeouts, and error handling. Here are both in action.
10+ Practical Examples
Since these examples involve HTTP calls to external APIs, we’ll use simulated responses where needed to keep the examples self-contained and testable. The patterns shown work identically with real APIs.
Example 1: Simple GET with URL.text
What we’re doing: Making the simplest possible GET request and parsing the JSON response.
Example 1: URL.text GET Request
import groovy.json.JsonSlurper
// The simplest GET request in Groovy - one line!
// def todo = new JsonSlurper().parseText(new URL('https://jsonplaceholder.typicode.com/todos/1').text)
// Simulated response for demonstration
def slurper = new JsonSlurper()
def responseText = '{"userId": 1, "id": 1, "title": "Buy groceries", "completed": false}'
def todo = slurper.parseText(responseText)
println "ID: ${todo.id}"
println "Title: ${todo.title}"
println "Completed: ${todo.completed}"
println "User ID: ${todo.userId}"
println "Type: ${todo.getClass().name}"
Output
ID: 1 Title: Buy groceries Completed: false User ID: 1 Type: org.apache.groovy.json.internal.LazyMap
What happened here: new URL(urlString).text is a Groovy GDK method that performs a GET request and returns the response body as a string. Combined with JsonSlurper.parseText(), you parse the API response in one line. This is the Groovy way – concise and readable. For more on JSON parsing, see our JsonSlurper guide.
Example 2: GET with HttpURLConnection
What we’re doing: Using HttpURLConnection for a GET request with full control over headers and response codes.
Example 2: HttpURLConnection GET
import groovy.json.JsonSlurper
def fetchJson(String urlString) {
def url = new URL(urlString)
def conn = url.openConnection() as HttpURLConnection
conn.requestMethod = 'GET'
conn.setRequestProperty('Accept', 'application/json')
conn.setRequestProperty('User-Agent', 'Groovy-Script/1.0')
conn.connectTimeout = 5000
conn.readTimeout = 5000
def responseCode = conn.responseCode
println "Response code: ${responseCode}"
if (responseCode == 200) {
def response = conn.inputStream.text
return new JsonSlurper().parseText(response)
} else {
def error = conn.errorStream?.text ?: 'No error body'
println "Error: ${error}"
return null
}
}
// Simulated for demonstration - in real code, pass a real URL
// def user = fetchJson('https://jsonplaceholder.typicode.com/users/1')
// Simulate the function output
println "Response code: 200"
def user = new JsonSlurper().parseText('''
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "sincere@april.biz",
"phone": "1-770-736-8031"
}
''')
println "Name: ${user.name}"
println "Username: ${user.username}"
println "Email: ${user.email}"
println "Phone: ${user.phone}"
Output
Response code: 200 Name: Leanne Graham Username: Bret Email: sincere@april.biz Phone: 1-770-736-8031
What happened here: HttpURLConnection gives you full control. We set the request method, added Accept and User-Agent headers, configured timeouts, and checked the response code before reading the body. The errorStream is used for non-2xx responses.
Example 3: Parsing a JSON Array Response
What we’re doing: Fetching a list endpoint that returns a JSON array and processing the results.
Example 3: JSON Array Response
import groovy.json.JsonSlurper
def slurper = new JsonSlurper()
// Simulated API response - a list of users
def usersJson = '''
[
{"id": 1, "name": "Alice", "department": "Engineering", "salary": 95000},
{"id": 2, "name": "Bob", "department": "Marketing", "salary": 72000},
{"id": 3, "name": "Charlie", "department": "Engineering", "salary": 88000},
{"id": 4, "name": "Diana", "department": "Sales", "salary": 68000},
{"id": 5, "name": "Eve", "department": "Engineering", "salary": 102000}
]
'''
def users = slurper.parseText(usersJson)
// Basic stats
println "Total users: ${users.size()}"
println "All names: ${users*.name}"
// Filter engineers
def engineers = users.findAll { it.department == 'Engineering' }
println "\nEngineers (${engineers.size()}):"
engineers.each { println " ${it.name} - \$${it.salary}" }
// Average salary
def avgSalary = users.sum { it.salary } / users.size()
println "\nAverage salary: \$${avgSalary}"
// Highest paid
def topEarner = users.max { it.salary }
println "Top earner: ${topEarner.name} (\$${topEarner.salary})"
// Group by department
def byDept = users.groupBy { it.department }
byDept.each { dept, members ->
println "\n${dept}: ${members*.name.join(', ')}"
}
Output
Total users: 5 All names: [Alice, Bob, Charlie, Diana, Eve] Engineers (3): Alice - $95000 Charlie - $88000 Eve - $102000 Average salary: $85000 Top earner: Eve ($102000) Engineering: Alice, Charlie, Eve Marketing: Bob Sales: Diana
What happened here: API list endpoints return JSON arrays, which JsonSlurper parses into Groovy lists. Once parsed, you get the full power of Groovy collection methods – findAll(), max(), groupBy(), the spread operator (*.). This is where Groovy truly shines for API consumption.
Example 4: POST Request with JSON Body
What we’re doing: Sending a POST request with a JSON body and reading the response.
Example 4: POST Request
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
def postJson(String urlString, Map data) {
def url = new URL(urlString)
def conn = url.openConnection() as HttpURLConnection
conn.requestMethod = 'POST'
conn.doOutput = true
conn.setRequestProperty('Content-Type', 'application/json')
conn.setRequestProperty('Accept', 'application/json')
conn.connectTimeout = 5000
conn.readTimeout = 5000
// Write JSON body
def jsonBody = JsonOutput.toJson(data)
conn.outputStream.withWriter('UTF-8') { writer ->
writer.write(jsonBody)
}
def responseCode = conn.responseCode
def responseBody = (responseCode >= 200 && responseCode < 300)
? conn.inputStream.text
: conn.errorStream?.text
conn.disconnect()
return [code: responseCode, body: new JsonSlurper().parseText(responseBody ?: '{}')]
}
// Simulated POST call
println "--- POST Request ---"
println "URL: https://jsonplaceholder.typicode.com/posts"
def payload = [title: 'Groovy REST Guide', body: 'Learning API calls', userId: 1]
println "Payload: ${JsonOutput.prettyPrint(JsonOutput.toJson(payload))}"
// Simulated response
println "\nResponse code: 201"
def response = new JsonSlurper().parseText('{"id": 101, "title": "Groovy REST Guide", "body": "Learning API calls", "userId": 1}')
println "Created ID: ${response.id}"
println "Title: ${response.title}"
println "User ID: ${response.userId}"
Output
--- POST Request ---
URL: https://jsonplaceholder.typicode.com/posts
Payload: {
"title": "Groovy REST Guide",
"body": "Learning API calls",
"userId": 1
}
Response code: 201
Created ID: 101
Title: Groovy REST Guide
User ID: 1
What happened here: For POST requests, you set doOutput = true and write the JSON body to conn.outputStream. The Content-Type: application/json header tells the server we’re sending JSON. We use JsonOutput.toJson() to serialize the map – see our JsonBuilder guide for more options.
Example 5: PUT and DELETE Requests
What we’re doing: Making PUT (update) and DELETE requests to complete the CRUD operations.
Example 5: PUT and DELETE
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
def makeRequest(String method, String urlString, Map body = null) {
def url = new URL(urlString)
def conn = url.openConnection() as HttpURLConnection
conn.requestMethod = method
conn.setRequestProperty('Accept', 'application/json')
conn.connectTimeout = 5000
if (body) {
conn.doOutput = true
conn.setRequestProperty('Content-Type', 'application/json')
conn.outputStream.withWriter('UTF-8') { it.write(JsonOutput.toJson(body)) }
}
return [code: conn.responseCode, method: method]
}
// Simulated PUT request
println "--- PUT (Update) ---"
def updateData = [id: 1, title: 'Updated Title', body: 'Updated content', userId: 1]
println "Method: PUT"
println "URL: https://api.example.com/posts/1"
println "Body: ${JsonOutput.toJson(updateData)}"
println "Response: 200 OK"
// Simulated DELETE request
println "\n--- DELETE ---"
println "Method: DELETE"
println "URL: https://api.example.com/posts/1"
println "Response: 200 OK"
// Simulated PATCH request
println "\n--- PATCH (Partial Update) ---"
def patchData = [title: 'Only the title changes']
println "Method: PATCH"
println "URL: https://api.example.com/posts/1"
println "Body: ${JsonOutput.toJson(patchData)}"
println "Response: 200 OK"
Output
--- PUT (Update) ---
Method: PUT
URL: https://api.example.com/posts/1
Body: {"id":1,"title":"Updated Title","body":"Updated content","userId":1}
Response: 200 OK
--- DELETE ---
Method: DELETE
URL: https://api.example.com/posts/1
Response: 200 OK
--- PATCH (Partial Update) ---
Method: PATCH
URL: https://api.example.com/posts/1
Body: {"title":"Only the title changes"}
Response: 200 OK
What happened here: The pattern is the same for all HTTP methods – set requestMethod, add headers, optionally write a body, and read the response. PUT sends a full replacement, PATCH sends a partial update, and DELETE removes the resource. This reusable makeRequest() function handles all methods.
Example 6: Custom Headers and Query Parameters
What we’re doing: Adding custom headers and building URLs with query parameters.
Example 6: Headers and Query Params
import groovy.json.JsonSlurper
// Helper: build URL with query parameters
def buildUrl(String baseUrl, Map params) {
if (!params) return baseUrl
def query = params.collect { k, v ->
"${URLEncoder.encode(k.toString(), 'UTF-8')}=${URLEncoder.encode(v.toString(), 'UTF-8')}"
}.join('&')
return "${baseUrl}?${query}"
}
// Build a URL with query parameters
def url = buildUrl('https://api.example.com/search', [
q: 'groovy json',
page: 1,
limit: 10,
sort: 'relevance'
])
println "Built URL: ${url}"
// Common headers for API calls
def headers = [
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'Groovy-Script/1.0',
'X-API-Version': '2.0',
'X-Request-ID': UUID.randomUUID().toString()
]
println "\nHeaders:"
headers.each { k, v ->
println " ${k}: ${v}"
}
// Simulated request with headers
println "\nResponse: 200 OK"
def results = new JsonSlurper().parseText('''
{
"query": "groovy json",
"total": 42,
"page": 1,
"results": [
{"title": "Groovy JSON Parsing Guide", "score": 9.5},
{"title": "JsonBuilder Tutorial", "score": 8.8}
]
}
''')
println "Query: ${results.query}"
println "Total: ${results.total}"
println "Results: ${results.results*.title}"
Output
Built URL: https://api.example.com/search?q=groovy+json&page=1&limit=10&sort=relevance Headers: Accept: application/json Content-Type: application/json User-Agent: Groovy-Script/1.0 X-API-Version: 2.0 X-Request-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 Response: 200 OK Query: groovy json Total: 42 Results: [Groovy JSON Parsing Guide, JsonBuilder Tutorial]
What happened here: Query parameters must be URL-encoded to handle special characters and spaces. The buildUrl() helper creates properly encoded URLs from a map. Custom headers are set with conn.setRequestProperty(key, value). The X-Request-ID header is useful for tracing requests through distributed systems.
Example 7: Error Handling and HTTP Status Codes
What we’re doing: Properly handling different HTTP status codes and network errors.
Example 7: Error Handling
import groovy.json.JsonSlurper
def safeApiCall(String urlString) {
try {
def url = new URL(urlString)
def conn = url.openConnection() as HttpURLConnection
conn.requestMethod = 'GET'
conn.connectTimeout = 5000
conn.readTimeout = 5000
conn.setRequestProperty('Accept', 'application/json')
def code = conn.responseCode
switch (code) {
case 200..299:
def body = new JsonSlurper().parseText(conn.inputStream.text)
return [success: true, code: code, data: body]
case 400:
return [success: false, code: code, error: 'Bad Request']
case 401:
return [success: false, code: code, error: 'Unauthorized']
case 403:
return [success: false, code: code, error: 'Forbidden']
case 404:
return [success: false, code: code, error: 'Not Found']
case 429:
return [success: false, code: code, error: 'Rate Limited']
case 500..599:
return [success: false, code: code, error: 'Server Error']
default:
return [success: false, code: code, error: 'Unexpected status']
}
} catch (SocketTimeoutException e) {
return [success: false, code: 0, error: "Timeout: ${e.message}"]
} catch (ConnectException e) {
return [success: false, code: 0, error: "Connection refused: ${e.message}"]
} catch (UnknownHostException e) {
return [success: false, code: 0, error: "Unknown host: ${e.message}"]
} catch (Exception e) {
return [success: false, code: 0, error: "${e.class.simpleName}: ${e.message}"]
}
}
// Simulated responses for different scenarios
def scenarios = [
[code: 200, desc: 'Success', data: '{"status": "ok"}'],
[code: 404, desc: 'Not Found', data: null],
[code: 401, desc: 'Unauthorized', data: null],
[code: 500, desc: 'Server Error', data: null]
]
scenarios.each { scenario ->
println "HTTP ${scenario.code}: ${scenario.desc}"
if (scenario.data) {
println " Data: ${scenario.data}"
} else {
println " Error handled gracefully"
}
}
Output
HTTP 200: Success
Data: {"status": "ok"}
HTTP 404: Not Found
Error handled gracefully
HTTP 401: Unauthorized
Error handled gracefully
HTTP 500: Server Error
Error handled gracefully
What happened here: Reliable API consumption means handling every possible failure. The safeApiCall() function handles HTTP status codes (400, 401, 403, 404, 429, 5xx) and network exceptions (timeouts, connection refused, DNS failures). Always return a consistent response structure so callers don’t need to handle exceptions themselves.
Example 8: Timeouts and Connection Configuration
What we’re doing: Configuring connection timeouts, read timeouts, and other connection properties.
Example 8: Timeout Configuration
import groovy.json.JsonSlurper
def configuredRequest(String urlString, Map options = [:]) {
def conn = new URL(urlString).openConnection() as HttpURLConnection
// Timeout configuration
conn.connectTimeout = options.connectTimeout ?: 5000 // 5 seconds default
conn.readTimeout = options.readTimeout ?: 10000 // 10 seconds default
// Follow redirects
conn.instanceFollowRedirects = options.followRedirects ?: true
// Caching
conn.useCaches = options.useCache ?: false
// Method
conn.requestMethod = options.method ?: 'GET'
// Headers
conn.setRequestProperty('Accept', 'application/json')
options.headers?.each { k, v ->
conn.setRequestProperty(k, v)
}
return conn
}
// Show configuration options
println "Connection Configuration Options:"
println " connectTimeout: Time to establish TCP connection (ms)"
println " readTimeout: Time to wait for response data (ms)"
println " followRedirects: Follow HTTP 3xx redirects automatically"
println " useCaches: Use cached responses"
println ""
// Example configurations
def configs = [
'Quick check': [connectTimeout: 2000, readTimeout: 3000],
'Patient request': [connectTimeout: 10000, readTimeout: 30000],
'No cache': [useCache: false, readTimeout: 5000],
'Custom headers': [headers: ['Authorization': 'Bearer token123', 'X-Custom': 'value']]
]
configs.each { name, config ->
println "${name}:"
config.each { k, v -> println " ${k}: ${v}" }
}
Output
Connection Configuration Options: connectTimeout: Time to establish TCP connection (ms) readTimeout: Time to wait for response data (ms) followRedirects: Follow HTTP 3xx redirects automatically useCaches: Use cached responses Quick check: connectTimeout: 2000 readTimeout: 3000 Patient request: connectTimeout: 10000 readTimeout: 30000 No cache: useCache: false readTimeout: 5000 Custom headers: headers: [Authorization:Bearer token123, X-Custom:value]
What happened here: Always set timeouts on HTTP connections. Without them, your script can hang indefinitely if the server doesn’t respond. connectTimeout limits the TCP handshake time, while readTimeout limits how long you wait for data. A reusable configuration function keeps your code DRY.
Example 9: Paginated API Responses
What we’re doing: Handling paginated APIs by fetching multiple pages and combining results.
Example 9: Pagination
import groovy.json.JsonSlurper
def slurper = new JsonSlurper()
// Simulate a paginated API
def simulatePage(int page, int perPage = 3) {
def allItems = (1..10).collect { [id: it, name: "Item ${it}"] }
def start = (page - 1) * perPage
def end = Math.min(start + perPage, allItems.size())
def pageItems = (start < allItems.size()) ? allItems[start..<end] : []
return [
page: page,
per_page: perPage,
total: allItems.size(),
total_pages: Math.ceil(allItems.size() / perPage).intValue(),
data: pageItems
]
}
// Fetch all pages
def allResults = []
def page = 1
def totalPages = 1
while (page <= totalPages) {
def response = simulatePage(page)
totalPages = response.total_pages
allResults.addAll(response.data)
println "Fetched page ${page}/${totalPages} (${response.data.size()} items)"
page++
}
println "\nTotal items collected: ${allResults.size()}"
println "Items: ${allResults*.name}"
// Alternative: recursive approach
def fetchAll(int pg = 1, List acc = []) {
def resp = simulatePage(pg)
acc.addAll(resp.data)
return (pg < resp.total_pages) ? fetchAll(pg + 1, acc) : acc
}
def allItems = fetchAll()
println "\nRecursive total: ${allItems.size()}"
Output
Fetched page 1/4 (3 items) Fetched page 2/4 (3 items) Fetched page 3/4 (3 items) Fetched page 4/4 (1 items) Total items collected: 10 Items: [Item 1, Item 2, Item 3, Item 4, Item 5, Item 6, Item 7, Item 8, Item 9, Item 10] Recursive total: 10
What happened here: Many APIs return paginated results. The while loop fetches pages until page > totalPages. We also showed a recursive approach using default parameters. In production, add rate limiting (sleep between requests) and error handling for failed pages.
Example 10: Retry Logic for Flaky APIs
What we’re doing: Implementing retry logic with exponential backoff for unreliable API endpoints.
Example 10: Retry Logic
import groovy.json.JsonSlurper
def withRetry(int maxRetries = 3, Closure action) {
def lastException = null
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return action(attempt)
} catch (Exception e) {
lastException = e
def delay = (long) Math.pow(2, attempt) * 100 // Exponential backoff
println " Attempt ${attempt} failed: ${e.message}. Retrying in ${delay}ms..."
if (attempt < maxRetries) {
Thread.sleep(delay)
}
}
}
throw new RuntimeException("All ${maxRetries} attempts failed", lastException)
}
// Simulate a flaky API
def callCount = 0
def flakyApi = { int attempt ->
callCount++
if (callCount <= 2) {
throw new IOException("Connection reset by peer")
}
return [status: 'success', data: 'Hello from attempt ' + attempt]
}
println "Calling flaky API with retry:"
try {
def result = withRetry(3, flakyApi)
println " Success: ${result}"
} catch (Exception e) {
println " Final failure: ${e.message}"
}
// Reset and simulate total failure
callCount = 0
println "\nCalling always-failing API:"
try {
withRetry(3) { attempt ->
throw new IOException("Server unavailable")
}
} catch (Exception e) {
println " Gave up after 3 attempts: ${e.message}"
}
Output
Calling flaky API with retry: Attempt 1 failed: Connection reset by peer. Retrying in 200ms... Attempt 2 failed: Connection reset by peer. Retrying in 400ms... Success: [status:success, data:Hello from attempt 3] Calling always-failing API: Attempt 1 failed: Server unavailable. Retrying in 200ms... Attempt 2 failed: Server unavailable. Retrying in 400ms... Attempt 3 failed: Server unavailable. Retrying in 800ms... Gave up after 3 attempts: All 3 attempts failed
What happened here: The withRetry() function wraps any API call with exponential backoff. Each retry waits longer (200ms, 400ms, 800ms). If all attempts fail, the last exception is thrown. This pattern is essential for production scripts that call external APIs.
Example 11: Reusable REST Client Class
What we’re doing: Building a reusable REST client class that encapsulates common patterns.
Example 11: REST Client Class
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
class SimpleRestClient {
String baseUrl
Map defaultHeaders = ['Accept': 'application/json', 'Content-Type': 'application/json']
int timeout = 5000
JsonSlurper slurper = new JsonSlurper()
Map get(String path, Map params = [:]) {
def url = buildUrl(path, params)
println " GET ${url}"
// In real code: return request('GET', url)
return [method: 'GET', path: path, params: params]
}
Map post(String path, Map body) {
println " POST ${baseUrl}${path}"
println " Body: ${JsonOutput.toJson(body)}"
// In real code: return request('POST', "${baseUrl}${path}", body)
return [method: 'POST', path: path, body: body]
}
Map put(String path, Map body) {
println " PUT ${baseUrl}${path}"
// In real code: return request('PUT', "${baseUrl}${path}", body)
return [method: 'PUT', path: path, body: body]
}
Map delete(String path) {
println " DELETE ${baseUrl}${path}"
// In real code: return request('DELETE', "${baseUrl}${path}")
return [method: 'DELETE', path: path]
}
private String buildUrl(String path, Map params) {
def url = "${baseUrl}${path}"
if (params) {
def query = params.collect { k, v ->
"${URLEncoder.encode(k.toString(), 'UTF-8')}=${URLEncoder.encode(v.toString(), 'UTF-8')}"
}.join('&')
url += "?${query}"
}
return url
}
}
// Usage
def client = new SimpleRestClient(baseUrl: 'https://api.example.com')
println "REST Client Demo:"
client.get('/users', [page: 1, limit: 10])
client.post('/users', [name: 'Alice', email: 'alice@example.com'])
client.put('/users/1', [name: 'Alice Updated'])
client.delete('/users/1')
Output
REST Client Demo:
GET https://api.example.com/users?page=1&limit=10
POST https://api.example.com/users
Body: {"name":"Alice","email":"alice@example.com"}
PUT https://api.example.com/users/1
DELETE https://api.example.com/users/1
What happened here: A reusable client class eliminates repetitive code. It handles URL construction, default headers, timeouts, and JSON serialization. In production, you’d add retry logic, logging, and authentication to this class. This pattern is common in Jenkins shared libraries and Groovy-based automation.
Example 12: Real-World – GitHub API Integration
What we’re doing: Demonstrating a real-world pattern for consuming the GitHub API.
Example 12: GitHub API Pattern
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
// Simulated GitHub API response
def slurper = new JsonSlurper()
def repos = slurper.parseText('''
[
{"name": "groovy", "full_name": "apache/groovy", "stargazers_count": 4800, "language": "Groovy", "open_issues_count": 250},
{"name": "gradle", "full_name": "gradle/gradle", "stargazers_count": 15300, "language": "Groovy", "open_issues_count": 1200},
{"name": "grails-core", "full_name": "grails/grails-core", "stargazers_count": 2700, "language": "Groovy", "open_issues_count": 180},
{"name": "spock", "full_name": "spockframework/spock", "stargazers_count": 3400, "language": "Groovy", "open_issues_count": 95},
{"name": "nextflow", "full_name": "nextflow-io/nextflow", "stargazers_count": 2200, "language": "Groovy", "open_issues_count": 310}
]
''')
println "=== Top Groovy Repositories ==="
println String.format("%-25s %8s %8s", "Repository", "Stars", "Issues")
println "-" * 45
repos.sort { -it.stargazers_count }.each { repo ->
println String.format("%-25s %8d %8d", repo.full_name, repo.stargazers_count, repo.open_issues_count)
}
println "\n--- Statistics ---"
println "Total repos: ${repos.size()}"
println "Total stars: ${repos.sum { it.stargazers_count }}"
println "Total issues: ${repos.sum { it.open_issues_count }}"
println "Avg stars: ${(repos.sum { it.stargazers_count } / repos.size()).round(0)}"
println "Most starred: ${repos.max { it.stargazers_count }.full_name}"
println "Most issues: ${repos.max { it.open_issues_count }.full_name}"
Output
=== Top Groovy Repositories === Repository Stars Issues --------------------------------------------- gradle/gradle 15300 1200 apache/groovy 4800 250 spockframework/spock 3400 95 grails/grails-core 2700 180 nextflow-io/nextflow 2200 310 --- Statistics --- Total repos: 5 Total stars: 28400 Total issues: 2035 Avg stars: 5680 Most starred: gradle/gradle Most issues: gradle/gradle
What happened here: This demonstrates a realistic API consumption workflow – fetch data, sort it, format it for display, and compute aggregate statistics. The actual GitHub API call would be: slurper.parseText(new URL('https://api.github.com/search/repositories?q=language:groovy&sort=stars').text). The Groovy collection methods make data processing a breeze.
Authentication Basics
Most APIs require authentication. Here are the common patterns in Groovy:
Authentication Patterns
import groovy.json.JsonSlurper
// 1. API Key in Header
def apiKeyAuth(HttpURLConnection conn, String apiKey) {
conn.setRequestProperty('X-API-Key', apiKey)
}
// 2. Bearer Token (OAuth2)
def bearerAuth(HttpURLConnection conn, String token) {
conn.setRequestProperty('Authorization', "Bearer ${token}")
}
// 3. Basic Authentication
def basicAuth(HttpURLConnection conn, String username, String password) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
conn.setRequestProperty('Authorization', "Basic ${credentials}")
}
// Demonstrate Base64 encoding for Basic Auth
def encoded = "admin:password123".bytes.encodeBase64().toString()
println "Basic Auth header: Basic ${encoded}"
// API Key example
println "\nAPI Key header: X-API-Key: sk-abc123def456"
// Bearer token example
println "Bearer header: Authorization: Bearer eyJhbGciOi..."
// Show how to read token from environment
def token = System.getenv('API_TOKEN') ?: 'not-set'
println "\nToken from env: ${token == 'not-set' ? '(not set - using default)' : 'loaded from API_TOKEN'}"
// Show how to read from a config file
println "\nBest practices:"
println " - Never hardcode credentials in scripts"
println " - Use environment variables or config files"
println " - Use .gitignore to exclude credential files"
println " - Rotate tokens regularly"
Output
Basic Auth header: Basic YWRtaW46cGFzc3dvcmQxMjM= API Key header: X-API-Key: sk-abc123def456 Bearer header: Authorization: Bearer eyJhbGciOi... Token from env: (not set - using default) Best practices: - Never hardcode credentials in scripts - Use environment variables or config files - Use .gitignore to exclude credential files - Rotate tokens regularly
Always load credentials from environment variables (System.getenv()) or config files – never hardcode them in your scripts.
Edge Cases and Best Practices
Edge Case: Non-JSON Responses
Handling Non-JSON Responses
import groovy.json.JsonSlurper
import groovy.json.JsonException
def safeParseJson(String text) {
try {
return [success: true, data: new JsonSlurper().parseText(text)]
} catch (JsonException e) {
return [success: false, error: "Invalid JSON: ${e.message.take(50)}"]
} catch (Exception e) {
return [success: false, error: "${e.class.simpleName}: ${e.message?.take(50)}"]
}
}
// Valid JSON
println safeParseJson('{"status": "ok"}')
// HTML error page (common when API returns 500)
println safeParseJson('<html><body>500 Internal Server Error</body></html>')
// Empty response
println safeParseJson('')
// Plain text
println safeParseJson('OK')
Output
[success:true, data:[status:ok]] [success:false, error:Invalid JSON: Unable to determine the current chara] [success:false, error:IllegalArgumentException: Text must not be null or ] [success:false, error:Invalid JSON: Unable to determine the current chara]
Best Practices
DO:
- Always set
connectTimeoutandreadTimeouton connections - Check the response code before reading the body
- Use
errorStreamfor non-2xx responses (it may contain useful error details) - Close connections with
conn.disconnect()when done - Load credentials from environment variables or config files
DON’T:
- Use
URL.textfor production code – it has no error handling or timeout control - Assume the response is always JSON – check Content-Type headers
- Hardcode API tokens or passwords in scripts
- Ignore rate limiting – add delays between bulk API calls
Common Pitfalls
Pitfall 1: No Timeout = Infinite Hang
Problem: Not setting timeouts causes scripts to hang indefinitely when a server doesn’t respond.
Pitfall 1: Missing Timeouts
// BAD - can hang forever
// def text = new URL('https://slow-server.example.com/data').text
// GOOD - always set timeouts
def conn = new URL('https://api.example.com/data').openConnection() as HttpURLConnection
conn.connectTimeout = 5000 // 5 second connect timeout
conn.readTimeout = 10000 // 10 second read timeout
println "Always set timeouts:"
println " connectTimeout: ${conn.connectTimeout}ms"
println " readTimeout: ${conn.readTimeout}ms"
Output
Always set timeouts: connectTimeout: 5000ms readTimeout: 10000ms
Solution: Always use HttpURLConnection with explicit timeouts for production code. Save URL.text for quick throwaway scripts only.
Pitfall 2: Reading Error Stream Incorrectly
Problem: Trying to read inputStream when the response is an error (4xx/5xx).
Pitfall 2: Wrong Stream
// BAD - throws IOException for 4xx/5xx responses
// def body = conn.inputStream.text
// GOOD - check response code first
def readResponse(HttpURLConnection conn) {
def code = conn.responseCode
if (code >= 200 && code < 300) {
return conn.inputStream.text
} else {
return conn.errorStream?.text ?: "Error ${code}: no body"
}
}
println "Response reading pattern:"
println " 1. Check conn.responseCode"
println " 2. Use conn.inputStream for 2xx responses"
println " 3. Use conn.errorStream for 4xx/5xx responses"
println " 4. errorStream can be null - use ?. operator"
Output
Response reading pattern: 1. Check conn.responseCode 2. Use conn.inputStream for 2xx responses 3. Use conn.errorStream for 4xx/5xx responses 4. errorStream can be null - use ?. operator
Solution: Always check the response code first. For error responses (4xx, 5xx), read from errorStream – not inputStream. And remember that errorStream can be null, so use ?..
Pitfall 3: URL Encoding Issues
Problem: Not encoding query parameters properly causes broken requests.
Pitfall 3: URL Encoding
// BAD - special characters break the URL
def badUrl = "https://api.example.com/search?q=groovy & json"
println "Bad URL: ${badUrl}"
// GOOD - encode query parameters
def query = URLEncoder.encode('groovy & json', 'UTF-8')
def goodUrl = "https://api.example.com/search?q=${query}"
println "Good URL: ${goodUrl}"
// Even better - use a helper function
def encodeParams(Map params) {
params.collect { k, v ->
"${URLEncoder.encode(k.toString(), 'UTF-8')}=${URLEncoder.encode(v.toString(), 'UTF-8')}"
}.join('&')
}
def params = [q: 'groovy & json', page: 1, filter: 'name=test']
println "Encoded: https://api.example.com/search?${encodeParams(params)}"
Output
Bad URL: https://api.example.com/search?q=groovy & json Good URL: https://api.example.com/search?q=groovy+%26+json Encoded: https://api.example.com/search?q=groovy+%26+json&page=1&filter=name%3Dtest
Solution: Always use URLEncoder.encode() for query parameter values. Special characters like &, =, ?, and spaces must be encoded to produce valid URLs.
Conclusion
Groovy REST API consumption is one of Groovy’s strongest use cases. From one-liner scripts using URL.text to production-ready clients with retry logic and authentication, Groovy gives you the tools to work with APIs efficiently and concisely.
The combination of HttpURLConnection for HTTP control, JsonSlurper for response parsing, and JsonOutput for request body generation covers the entire API workflow. Add Groovy’s collection methods for data processing, and you have a complete API integration toolkit with zero external dependencies.
For the JSON fundamentals that underpin this guide, review our Groovy JSON Parsing with JsonSlurper and Groovy JSON Output with JsonBuilder guides.
Summary
new URL(url).textis the simplest GET request – great for quick scriptsHttpURLConnectiongives full control over method, headers, body, and timeouts- Always set
connectTimeoutandreadTimeoutin production code - Check response codes before reading – use
errorStreamfor non-2xx responses - Use
URLEncoder.encode()for query parameters - Load credentials from environment variables – never hardcode them
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy CSV Parsing and Generation
Frequently Asked Questions
How do I make a GET request in Groovy?
The simplest way is new URL('https://api.example.com/data').text, which returns the response as a string. For more control, use HttpURLConnection: open the connection, set request method to GET, configure timeouts, and read conn.inputStream.text. Combine with new JsonSlurper().parseText() to parse JSON responses.
How do I send a POST request with JSON body in Groovy?
Use HttpURLConnection with requestMethod = 'POST' and doOutput = true. Set the Content-Type header to application/json, then write the JSON body using conn.outputStream.withWriter { it.write(JsonOutput.toJson(data)) }. Read the response from conn.inputStream.text after checking the response code.
How do I handle API errors in Groovy?
Check conn.responseCode before reading the response. For 2xx responses, read from conn.inputStream. For 4xx/5xx errors, read from conn.errorStream (use ?. as it can be null). Wrap the entire call in try-catch to handle network exceptions like SocketTimeoutException and ConnectException.
How do I add authentication to Groovy API calls?
For API keys, set a custom header: conn.setRequestProperty('X-API-Key', key). For Bearer tokens (OAuth2): conn.setRequestProperty('Authorization', 'Bearer ' + token). For Basic Auth: encode credentials with 'user:pass'.bytes.encodeBase64() and set the Authorization header. Always load credentials from environment variables.
Should I use URL.text or HttpURLConnection for API calls?
Use URL.text for quick scripts and one-off calls where error handling isn’t critical. Use HttpURLConnection for production code – it gives you control over HTTP method, headers, timeouts, request body, and response codes. URL.text has no timeout, no error handling, and only supports GET requests.
Related Posts
Previous in Series: Groovy JSON Output with JsonBuilder
Next in Series: Groovy CSV Parsing and Generation
Related Topics You Might Like:
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment