DevLearningTools

MODULE 18 · LESSON 07

REST API Project

A complete user-management REST API in ColdFusion, walked through file by file from its real source: five CFC methods mapped to HTTP verbs, parameterized queries, JSON bodies, and a security review that finds the missing authentication and status-code problems.

This is the final Practice Project, a complete user CRUD API. Every code block is copied from the project's real source, followed by what the code does and what it's missing. The API is small enough to read in one sitting, which makes its gaps easy to see.

Learning Objectives

After working through this project, you'll be able to:

  • Map CFC methods to GET, POST, PUT, and DELETE with restPath and httpMethod.
  • Read a JSON body with getHttpRequestData() and deserializeJson().
  • Run parameterized queries for every input.
  • Return correct HTTP status codes, not 200 for everything.
  • Find the authentication and error-handling gaps in a working API.

How a Request Flows Through the API

Client Sends a Request

e.g. PUT /rest/api/users/update/3

REST Routes It to a Method

matched by restPath and httpMethod

Method Runs a Parameterized Query

cfqueryparam on every input

JSON Comes Back

always HTTP 200 in this version

The Project's Files

FileRole
Application.cfcSession and debug settings, the api mapping, and REST settings
api/users.cfcThe five REST methods
db/db.cfcA helper that gets a datasource through the Java service factory, not used by users.cfc
sql/create_users.sqlThe users table (SQL Server syntax)

Step 1: Application.cfc

Application.cfc maps the api folder and sets REST settings. It also turns on debug output, which the audit flags.

Application.cfc
<cfcomponent output="false">
    <cfset this.name = "RestAPIApp">
    <cfset this.applicationTimeout = createTimeSpan(1, 0, 0, 0)>
    <cfset this.sessionManagement = true>
    <cfset this.sessionTimeout = createTimeSpan(0, 0, 30, 0)>
    <cfset this.mappings["/api"] = expandPath("./api")>

    <cfsetting showDebugOutput="true">
    <cfset this.showDebugOutput = true>

    <cfset this.restSettings = {
        cfclocation = "api",
        restEnabled = true,
        restPath = "/rest/api"
    }>
</cfcomponent>
NOTE

Routing for the API is also registered in the ColdFusion Administrator (Data & Services > REST Services), as the README describes. The this.restSettings struct here wasn't found on cfdocs.org, so don't rely on it without checking your ColdFusion version.

Step 2: The Database Table

sql/create_users.sql
CREATE TABLE users (
    id INT IDENTITY(1,1) PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100) UNIQUE,
    phone VARCHAR(20)
);
NOTE

IDENTITY(1,1) is SQL Server syntax. The email column is UNIQUE, which means a duplicate email fails at the database.

Step 3: The Five Endpoints

Everything the API does lives in one component. Each method is marked with rest-path metadata, so ColdFusion routes the matching HTTP request to it.

MethodPathWhat it does
GET/rest/api/usersLists every row
GET/rest/api/users/profile/{id}Returns one user, or an error struct
POST/rest/api/users/createInserts a user from the JSON body
PUT/rest/api/users/update/{id}Updates a user from the JSON body
DELETE/rest/api/users/delete/{id}Deletes a user
api/users.cfc
<cfcomponent rest="true" restpath="users" output="false">

    <!--- List All Users --->
    <cffunction name="getUsers" access="remote" returnType="array" httpMethod="GET" restPath="">
        <cftry>
            <cfquery name="users" datasource="usercrud">
                SELECT * FROM users
            </cfquery>

            <!--- Convert query to array of structs --->
            <cfset var result = []>
            <cfloop query="users">
                <cfset row = {}>
                <cfloop list="#users.columnList#" index="col">
                    <cfset row[col] = users[col][currentRow]>
                </cfloop>
                <cfset arrayAppend(result, row)>
            </cfloop>

            <cfreturn result>
            
        <cfcatch>
            <cfreturn { "error": cfcatch.message, "detail": cfcatch.detail }>
        </cfcatch>
        </cftry>
    </cffunction>

    <!--- Get One User --->
    <cffunction name="getUser" access="remote" returnType="struct" httpMethod="GET" restPath="profile/{id}">
        <cfargument name="id" type="numeric" required="true" restargsource="path">
        <cfquery name="user" datasource="usercrud">
            SELECT * FROM users WHERE id = <cfqueryparam value="#id#" cfsqltype="cf_sql_integer">
        </cfquery>
        <cfif user.recordCount>
            <cfreturn user.getRow(1)>
        <cfelse>
            <cfreturn {error="User not found."}>
        </cfif>
    </cffunction>

    <!--- Create User --->
    <cffunction name="createUser" access="remote" returnType="any" httpMethod="POST" restPath="create">
        <cftry>
            <cfset var body = deserializeJson(toString(getHttpRequestData().content))>

            <cfquery datasource="usercrud">
                INSERT INTO users (name, email, phone)
                VALUES (
                    <cfqueryparam value="#body.name#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#body.email#" cfsqltype="cf_sql_varchar">,
                    <cfqueryparam value="#body.phone#" cfsqltype="cf_sql_varchar">
                )
            </cfquery>

            <cfreturn { success=true, message="User created successfully" }>

        <cfcatch>
            <cfreturn { error=true, message=cfcatch.message, detail=cfcatch.detail }>
        </cfcatch>
        </cftry>
    </cffunction>

    <!--- Update User --->
    <cffunction name="updateUser" access="remote" returnType="any" httpMethod="PUT" restPath="update/{id}">
        <cfargument name="id" type="numeric" required="true" restargsource="path">
        <cftry>
            <cfset var body = deserializeJson(toString(getHttpRequestData().content))>

            <cfquery datasource="usercrud">
                UPDATE users
                SET
                    name = <cfqueryparam value="#body.name#" cfsqltype="cf_sql_varchar">,
                    email = <cfqueryparam value="#body.email#" cfsqltype="cf_sql_varchar">,
                    phone = <cfqueryparam value="#body.phone#" cfsqltype="cf_sql_varchar">
                WHERE id = <cfqueryparam value="#id#" cfsqltype="cf_sql_integer">
            </cfquery>

            <cfreturn { success=true, message="User updated successfully" }>

        <cfcatch>
            <cfreturn { error=true, message=cfcatch.message, detail=cfcatch.detail }>
        </cfcatch>
        </cftry>
    </cffunction>

    <!--- Delete User --->
    <cffunction name="deleteUser" access="remote" returnType="any" httpMethod="DELETE" restPath="delete/{id}">
        <cfargument name="id" type="numeric" required="true" restargsource="path">
        <cftry>
            <cfquery datasource="usercrud">
                DELETE FROM users WHERE id = <cfqueryparam value="#id#" cfsqltype="cf_sql_integer">
            </cfquery>
            <cfreturn { success=true, message="User deleted successfully" }>
        <cfcatch>
            <cfreturn { error=true, message=cfcatch.message, detail=cfcatch.detail }>
        </cfcatch>
        </cftry>
    </cffunction>


</cfcomponent>

Step 4: The Unused Helper

db/db.cfc gets a datasource through ColdFusion's Java service factory. users.cfc never calls it. Every query uses datasource="usercrud" directly.

db/db.cfc
<cfcomponent>

    <cffunction name="getConnection" access="public" returnType="any">
        <cfset var conn = createObject("java", "coldfusion.server.ServiceFactory")
                    .getDataSourceService().getDataSource("usercrud")>
        <cfreturn conn>
    </cffunction>

</cfcomponent>
NOTE

Unused code is a maintenance trap. Either use this helper everywhere, or delete it.

Security Review of This Code

FindingWhereWhy it matters
No authentication on any methodapi/users.cfc, all five methodsAnyone can read, change, and delete every user, including emails and phone numbers
Every response is HTTP 200, errors includedAll methodsA client can't tell a missing user or a failed insert from a success by status code
updateUser and deleteUser report success even when no row matchedupdateUser, deleteUserA PUT or DELETE for an id that doesn't exist says it worked
Full exception detail returned to the clientEvery catch blockcfcatch.detail can include SQL fragments, table names, and paths
getUsers returns SELECT * with no limitgetUsersEvery column and every row go out in one response, with nothing to stop a huge result set
Debug output on in Application.cfcshowDebugOutput = trueInternal details appear on every response
No input validation on create or updatecreateUser, updateUserA missing or empty email goes straight into the database, where UNIQUE rejects a duplicate
db/db.cfc is never calleddb/db.cfcDead code that suggests a second database path that doesn't exist
NOTE

The good part: every query uses cfqueryparam, so none of the inputs can inject SQL. Keep that.

Fixes

The fixes below change the real methods. Authentication comes first, since nothing else matters while anyone can call every method. The token is read from the Authorization header with getHttpRequestData() and must come from configuration, not from code.

api/users.cfc (authorization helper, added to the component)
private boolean function isAuthorized() {
    var headers = getHttpRequestData().headers;
    var auth = structKeyExists(headers, "Authorization") ? headers["Authorization"] : "";
    return auth EQ "Bearer " & application.apiToken;
}
api/users.cfc (getUser with auth and 404)
<cffunction name="getUser" access="remote" returnType="any" httpMethod="GET" restPath="profile/{id}">
    <cfargument name="id" type="numeric" required="true" restargsource="path">
    <cfif NOT isAuthorized()>
        <cfheader statuscode="401" statustext="Unauthorized">
        <cfreturn { error = true, message = "Unauthorized" }>
    </cfif>
    <cfquery name="user" datasource="usercrud">
        SELECT id, name, email, phone FROM users WHERE id = <cfqueryparam value="#arguments.id#" cfsqltype="cf_sql_integer">
    </cfquery>
    <cfif user.recordCount>
        <cfreturn user.getRow(1)>
    </cfif>
    <cfheader statuscode="404" statustext="Not Found">
    <cfreturn { error = true, message = "User not found." }>
</cffunction>
api/users.cfc (updateUser checks the row exists first)
<cfquery name="existing" datasource="usercrud">
    SELECT id FROM users WHERE id = <cfqueryparam value="#arguments.id#" cfsqltype="cf_sql_integer">
</cfquery>
<cfif NOT existing.recordCount>
    <cfheader statuscode="404" statustext="Not Found">
    <cfreturn { error = true, message = "User not found." }>
</cfif>
api/users.cfc (error handling, log on the server, return a generic message)
<cfcatch>
    <cflog file="usersapi" type="error" text="#cfcatch.message# | #cfcatch.detail#">
    <cfheader statuscode="500" statustext="Internal Server Error">
    <cfreturn { error = true, message = "Internal error" }>
</cfcatch>
Application.cfc (debug off outside development)
<cfset this.showDebugOutput = false>
NOTE

Two things to verify on your own server before relying on these snippets: that cfheader sets the status on REST responses in your ColdFusion version, and that the Authorization header key arrives with the case you check for.

Common Beginner Mistakes

Returning 200 for every response

The status code is part of the API contract. A 404 for a missing user and a 201 for a created one tell clients what happened without them parsing the body.

Leaving an API open because it's only for testing

Test endpoints get deployed, and this one exposes personal data. Add authentication before the first deployment.

Returning exception details to the client

Log them on the server. The client needs a message it can act on, not SQL.

Interview Questions

Why return 404 for a missing user instead of 200 with an error body?

HTTP clients and caches read the status code first. A 200 with an error in the body forces every client to parse the body to learn that the request failed.

What's the main risk of an unauthenticated user API?

Anyone can read, change, or delete the data, including personal details like email and phone numbers.

Summary

You read the whole API, mapped its five methods to HTTP verbs, saw where parameterized queries protect it, and found that it has no authentication, always returns 200, and reports success for changes that touched nothing. The fixes are short, and they're the difference between a demo and an API you could deploy.