DevLearningTools

MODULE 14 · LESSON 03

Consuming APIs

Calling an external REST API from ColdFusion with cfhttp: GET and POST requests, headers and JSON bodies via cfhttpparam, deserializeJSON, and a real gotcha with throwonerror's default that silently breaks status-code checking.

New lessons are added one at a time as the course gets built out — a graded quiz for each lesson is still on the way.

The previous lessons built a REST API. This one calls one, from ColdFusion, using cfhttp to send the request and deserializeJSON to turn the response back into a struct or array your code can actually work with.

Learning Objectives

After completing this lesson, you'll be able to:

  • Perform a GET request and parse the JSON response.
  • Send a POST request with a JSON body and the right headers.
  • Check the response status code before trusting the result, and know why that check might silently never run.

How a Request Actually Gets Made and Parsed

cfhttp Sends the Request

External API Processes It

JSON Response Returned

deserializeJSON() Converts It to a Struct/Array

A Basic GET Request

CFScript
cfhttp(method = "GET", url = "https://api.example.com/users", result = "apiResponse") {
    cfhttpparam(type = "url", name = "page", value = "1");
    cfhttpparam(type = "url", name = "limit", value = "20");
}

data = deserializeJSON(apiResponse.fileContent);
Tag Syntax
<cfhttp method="GET" url="https://api.example.com/users" result="apiResponse">
    <cfhttpparam type="url" name="page" value="1">
    <cfhttpparam type="url" name="limit" value="20">
</cfhttp>

<cfset data = deserializeJSON(apiResponse.fileContent)>
NOTE

cfhttpparam type="url" appends a query-string pair to the request URL, this example produces GET /users?page=1&limit=20.

What's in the Result Struct

FieldMeaning
statusCodeThe HTTP status code and reason string, e.g. "200 OK"
fileContentThe response body (usually a string)
responseHeaderA structure of the response's headers
mimeTypeThe Content-Type of the response
errorDetailAn error message, if something went wrong

cfhttpparam's Types

typeSends
headerA custom HTTP header (not URL-encoded)
urlA query-string name/value pair appended to the request URL
bodyThe raw request body, as-is
formfieldA form field, URL-encoded, simulating a standard HTML form POST
xmlA request body with Content-Type: text/xml
cookieA cookie sent as a header, URL-encoded
fileThe contents of a specified file

A Real Example: POST With a JSON Body and Auth Header

CFScript
payload = { name: "John Doe", email: "john@example.com" };

cfhttp(method = "POST", url = "https://api.example.com/users", result = "apiResponse") {
    cfhttpparam(type = "header", name = "Authorization", value = "Bearer #accessToken#");
    cfhttpparam(type = "header", name = "Content-Type", value = "application/json");
    cfhttpparam(type = "body", value = serializeJSON(payload));
}
NOTE

Setting Content-Type: application/json explicitly matters, without it, some APIs won't correctly interpret a JSON body sent as type="body".

A Real Gotcha: throwonerror Defaults to true

cfhttp's throwonerror attribute defaults to true, meaning an error response code throws an exception rather than just returning it in statusCode for you to inspect. Code that checks apiResponse.statusCode after the call, expecting to branch on a 404 or 500 there, never reaches that check at all unless throwonerror is explicitly set to false, the exception interrupts execution first.

CFScript
cfhttp(method = "GET", url = "https://api.example.com/users/999", result = "apiResponse", throwonerror = false) {
}

if (listFirst(apiResponse.statusCode, " ") == "200") {
    data = deserializeJSON(apiResponse.fileContent);
} else {
    writeLog(type = "error", text = "API error: #apiResponse.statusCode# - #apiResponse.fileContent#");
}
NOTE

listFirst(statusCode, " ") pulls just the numeric code ("200") out of the full status string ("200 OK") for a clean comparison.

Real Extras on Lucee's http Tag

AttributeWhat It Adds
cachedWithinCaches a response for a given timespan, or for the current request, avoiding a repeat call for identical parameters
connectionTimeout / socketTimeoutSeparate timeouts for the TCP handshake versus the actual data transfer (Lucee 6.2.2.66+)
autoCertAutomatically installs SSL certificates for HTTPS connections (Lucee 6.1.0.132+)
redirect="Lax"Returns the file content for a POST/DELETE redirect instead of dropping it (Lucee 7.0.0.208+)
NOTE

None of these have a direct equivalent on Adobe ColdFusion's cfhttp.

Common Beginner Mistakes

Checking statusCode without setting throwonerror="false"

By default, an error response throws an exception instead of letting the code reach the statusCode check at all. Either set throwonerror="false" and check statusCode, or wrap the call in cftry/cfcatch instead.

Sending a JSON body without setting Content-Type

Some APIs won't correctly parse the body as JSON without that header explicitly set to application/json.

Assuming deserializeJSON always returns a struct

It returns whatever the JSON actually represents, a struct for a JSON object, an array for a JSON array. Code that assumes struct-only access can fail on a top-level array response.

Putting API-calling logic directly inside a .cfm page

This makes authentication, error handling, timeouts, and reuse harder to manage consistently. A dedicated CFC/service layer for API calls keeps that logic in one place.

Best Practices

  • Set throwonerror="false" explicitly when you intend to branch on statusCode yourself, rather than relying on exception handling.
  • Always set Content-Type when sending a JSON body.
  • Put API-calling logic in a dedicated CFC rather than inline in a .cfm page, for reusable authentication, error handling, and logging.
  • On Lucee, use cachedWithin for GET requests that don't need a fresh call every single time.

Interview Questions

What does cfhttp's throwonerror default to, and why does that matter?

It defaults to true, meaning an error response throws an exception rather than letting you inspect statusCode afterward. Code that checks statusCode without setting throwonerror="false" never reaches that check on a real error.

What's the difference between cfhttpparam type="url" and type="formfield"?

type="url" appends a query-string parameter to the request URL. type="formfield" sends a URL-encoded form field in the request body, simulating a standard HTML form submission.

Why explicitly set the Content-Type header when POSTing JSON?

Without it, some APIs won't correctly interpret a raw type="body" payload as JSON, even if the content itself is valid JSON.

What's a real Lucee-specific feature that has no Adobe cfhttp equivalent?

cachedWithin, which caches a response for a given timespan (or the current request) to avoid repeating an identical call.

Summary

In this lesson, you performed a GET request with query parameters, sent a POST request with a JSON body and an Authorization header, parsed responses with deserializeJSON, and covered a real gotcha with throwonerror's default that silently prevents status-code checking from ever running.

What's Next?

The next lesson covers JSON responses in more depth: building and returning well-structured JSON from your own ColdFusion endpoints.

MORE IN APIS & MODERN DEVELOPMENT