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
| File | Role |
|---|---|
| Application.cfc | Session and debug settings, the api mapping, and REST settings |
| api/users.cfc | The five REST methods |
| db/db.cfc | A helper that gets a datasource through the Java service factory, not used by users.cfc |
| sql/create_users.sql | The 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.
<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>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
CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100) UNIQUE,
phone VARCHAR(20)
);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.
| Method | Path | What it does |
|---|---|---|
| GET | /rest/api/users | Lists every row |
| GET | /rest/api/users/profile/{id} | Returns one user, or an error struct |
| POST | /rest/api/users/create | Inserts 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 |
<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.
<cfcomponent>
<cffunction name="getConnection" access="public" returnType="any">
<cfset var conn = createObject("java", "coldfusion.server.ServiceFactory")
.getDataSourceService().getDataSource("usercrud")>
<cfreturn conn>
</cffunction>
</cfcomponent>Unused code is a maintenance trap. Either use this helper everywhere, or delete it.
Security Review of This Code
| Finding | Where | Why it matters |
|---|---|---|
| No authentication on any method | api/users.cfc, all five methods | Anyone can read, change, and delete every user, including emails and phone numbers |
| Every response is HTTP 200, errors included | All methods | A 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 matched | updateUser, deleteUser | A PUT or DELETE for an id that doesn't exist says it worked |
| Full exception detail returned to the client | Every catch block | cfcatch.detail can include SQL fragments, table names, and paths |
| getUsers returns SELECT * with no limit | getUsers | Every column and every row go out in one response, with nothing to stop a huge result set |
| Debug output on in Application.cfc | showDebugOutput = true | Internal details appear on every response |
| No input validation on create or update | createUser, updateUser | A missing or empty email goes straight into the database, where UNIQUE rejects a duplicate |
| db/db.cfc is never called | db/db.cfc | Dead code that suggests a second database path that doesn't exist |
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.
private boolean function isAuthorized() {
var headers = getHttpRequestData().headers;
var auth = structKeyExists(headers, "Authorization") ? headers["Authorization"] : "";
return auth EQ "Bearer " & application.apiToken;
}<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><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><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><cfset this.showDebugOutput = false>
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.