flxbl-apex-api

Availability

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 plugs

  • halted – 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:

  1. PlugJsonBodyDeserializer – parses the JSON body into ComplaintPayload and sets conn.payload

  2. PlugValidator – runs your validation rules; halts with 400 if invalid

  3. CreateComplaintPlug – your business logic; sets conn.model with the response

  4. PlugError (.always()) – converts any uncaught exception into an error response

  5. HttpJsonView (.always()) – serializes conn.model to JSON and writes to response.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 mock

  • Stubs 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:

Layer
Plugs
Purpose

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:

Category
Plugs

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:

Method
Description

.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:

Method
Description

.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:

Method
Description

.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:

Method
Description

.use(validator)

Chain multiple validators (errors are merged)

.skipWhenPayloadNull(true)

Skip validation if payload is null

Behavior:

  • Halts with 400 and throws ValidationException if validation fails

  • Works with PlugError to produce structured error responses

Implementing a Validator:

PlugSecureHeaders

Adds security-related HTTP headers to responses.

Default headers:

Header
Default Value
Purpose

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:

Method
Description

.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:

Method
Description

.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:

Method
Description

.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:

Method
Description

.suppressNulls(true)

Omit properties with null values from JSON

Behavior:

  • Sets Content-Type: application/json

  • Serializes whatever is in conn.model

Last updated