flxbl-apex-api
From
December 25
Building REST APIs in Apex often leads to repetitive boilerplate: parsing request bodies, validating headers, extracting path parameters, handling errors, and serializing responses. This scattered logic becomes hard to test, harder to reuse, and a maintenance burden as APIs grow. flxbl-apex-api brings the pipeline pattern to Salesforce, a single Conn object flows through composable Plug components, each doing one thing well:
Parse JSON in one plug,
Check authentication in another,
Run your business logic,
Then render the response (all in a declarative chain).
The result is APIs that are easy to read, trivial to test in isolation, and effortless to extend without touching existing code.
Core Concepts
The framework has three building blocks: Conn, Plug, and HttpPipeline.
Conn – The Connection Token
Conn is a single object that carries everything about the current request through the pipeline. It wraps Salesforce's RestContext.request and RestContext.response, and adds:
payload– the deserialized request body (set by a parsing plug)model– the response data to serialize (set by your handler)pathParams– extracted URL parameters like{accountId}assigns– a general-purpose map for passing data between plugshalted– a flag that stops further processing when something goes wrong
Think of Conn as a baton passed from plug to plug—each plug reads what it needs, writes what it produces, and hands it to the next.
Plug – A Single Responsibility
A Plug is any class that implements one method:
Each plug does one thing: validate a header, parse JSON, enforce auth, fetch data, render output. Because plugs are small and focused, they're easy to test independently and reuse across endpoints.
HttpPipeline – The Orchestrator
HttpPipeline chains plugs together and runs them in order:
When you call .call(), the pipeline creates a Conn and passes it through each plug. If any plug halts or throws, subsequent plugs are skipped—unless marked .always() (useful for error handlers and views that must always render a response).
Quick Start
Here's a minimal POST endpoint that validates input, runs business logic, and returns JSON:
What's happening:
PlugJsonBodyDeserializer– parses the JSON body intoComplaintPayloadand setsconn.payloadPlugValidator– runs your validation rules; halts with 400 if invalidCreateComplaintPlug– your business logic; setsconn.modelwith the responsePlugError(.always()) – converts any uncaught exception into an error responseHttpJsonView(.always()) – serializesconn.modelto JSON and writes toresponse.responseBody
Testing
The pipeline pattern makes testing straightforward at two levels: unit-testing individual plugs and integration-testing endpoints with stubbed dependencies.
Unit Testing a Plug
Each plug is a simple class with one method. Test it by creating a Conn, calling the plug, and asserting the result:
No HTTP mocking, no complex setup—just instantiate, call, assert.
Integration Testing with Stubs
When testing the full endpoint, you may want to stub out certain plugs (e.g., skip real validation, mock external calls). Use HttpPipeline.stub():
Key points:
HttpPipeline.stub(PlugType.class, mockInstance)– replaces any plug of that type with your mockStubs are only active during
Test.isRunningTest()Always call
HttpPipeline.clearStubs()in cleanup to avoid test pollution
A more complete example
A production-ready endpoint typically includes observability, security headers, content negotiation, and structured error handling:
What each layer does:
Observability
PlugTimer, PlugRequestId
Track timing and correlate logs
Security
PlugSecureHeaders
Add X-Frame-Options, Cache-Control, etc.
Content Negotiation
PlugAccept, PlugContentType
Validate client can send/receive JSON
Parsing
PlugJsonBodyDeserializer
Deserialize JSON into typed DTO
Validation
PlugValidator
Run business rules, halt on errors
Business Logic
CreateComplaintPlug
Your domain logic
Response
PlugError, HttpJsonView
Format errors and serialize response
Built-in Plugs
The framework ships with plugs for common API tasks. They fall into a few categories:
Parsing
PlugJsonBodyDeserializer, PlugPathParams
Validation
PlugAccept, PlugContentType, PlugValidator
Security
PlugSecureHeaders
Observability
PlugRequestId, PlugTimer
Error Handling
PlugError
Response
HttpJsonView
PlugJsonBodyDeserializer
Parses the JSON request body and stores the result in conn.payload.
Options:
.useUntyped(true)
Deserialize into Map<String, Object> instead of a typed class
.substituteKeyName('class')
Renames reserved words in JSON (e.g., "class" → "class_Z" in your DTO)
Behavior:
Skips if body is null or empty
Halts with 400 if JSON is malformed
PlugPathParams
Extracts named parameters from the URL path into conn.pathParams.
For a request to /services/apexrest/api/users/001ABC/orders/ORD123:
PlugAccept
Validates the Accept header to ensure the client accepts your response format.
Options:
.accept('application/json')
Add an acceptable media type
.allowMissingAcceptHeader(true)
Don't reject requests without an Accept header
Behavior:
Halts with 406 Not Acceptable if client doesn't accept any allowed type
PlugContentType
Validates the Content-Type header on requests with bodies (POST, PUT, PATCH).
Options:
.allow('text/xml')
Add an allowed content type (supports wildcards like application/*)
.allowMissingContentType(true)
Don't reject requests without Content-Type header
.requireBodyForMethods(methods)
Customize which HTTP methods require validation
Behavior:
Skips validation for GET, DELETE, etc.
Halts with 415 Unsupported Media Type if content type isn't allowed
PlugValidator
Runs validation logic against conn.payload using your Validator implementation.
Options:
.use(validator)
Chain multiple validators (errors are merged)
.skipWhenPayloadNull(true)
Skip validation if payload is null
Behavior:
Halts with 400 and throws
ValidationExceptionif validation failsWorks with
PlugErrorto produce structured error responses
Implementing a Validator:
PlugSecureHeaders
Adds security-related HTTP headers to responses.
Default headers:
X-Content-Type-Options
nosniff
Prevents MIME-type sniffing
X-Frame-Options
DENY
Prevents clickjacking via iframes
Cache-Control
no-store
Prevents caching sensitive data
Options:
.withXFrameOptions('SAMEORIGIN')
Customize X-Frame-Options
.withCacheControl('private')
Customize Cache-Control
.disableCacheControl()
Don't set Cache-Control header
PlugRequestId
Generates or extracts a unique request ID for correlation and tracing.
Options:
.fromHeader('X-Correlation-Id')
Use a custom header name (default: X-Request-Id)
.useClientHeader(false)
Always generate a new ID, ignore client header
.setResponseHeader(false)
Don't echo the ID back in response headers
Retrieving the ID:
The ID is automatically included in error responses when using PlugError.
PlugTimer
Captures request timing for performance monitoring.
Retrieving duration:
PlugError
Converts exceptions and halted states into consistent JSON error responses. Must be marked .always() to ensure it runs even when the pipeline halts.
Options:
.includeStackTrace(true)
Include stack trace (dev/sandbox only!)
.withRequestId('abc-123')
Manually set request ID (auto-detected from PlugRequestId)
.withFormatter(new MyErrorFormatter())
Use a custom error formatter
Default error response:
HttpJsonView
Serializes conn.model to JSON and writes it to the response body. Typically marked .always() so it runs even after errors.
Options:
.suppressNulls(true)
Omit properties with null values from JSON
Behavior:
Sets
Content-Type: application/jsonSerializes whatever is in
conn.model
Last updated