Chapter scope#
- Why standard methods can’t cover all possible scenarios
- Using custom methods for cases where side effects are necessary
- How to use stateless custom methods for computation-focused API methods
- Determining the target for a custom method (resource versus collection)
Often there will be actions on API resources that don’t fit nicely into standard methods. While such behaviors could be shoved into standard update/create methods, doing so bends the model, produces surprising interfaces, and increases complexity. This chapter explores how to support those actions safely and clearly using custom methods.
Motivation#
In most APIs, there will come a time when we need the ability to express a specific action that doesn’t really fit very well in one of the standard methods. For example, what’s the right API for sending an email or translating some text on the fly? What if you’re prohibited from storing any data (perhaps you’re translating text for the CIA)? While you can mash these actions into a standard method (most likely either a create action or an update action), at that point you’re sort of bending the framework of these standard methods to accommodate something that doesn’t quite fit. This leads to an obvious question: should we try to jam the behavior we want into the existing methods, bending the framework a bit to fit our needs? Or should we change our behavior into a form that fits a bit more cleanly within the framework? Or should we change the framework to accommodate our new scenario?
The answer to this depends on the scenario, but in this chapter we’ll explore custom methods as one potential solution to this conundrum. Essentially, this goes with the third option where we change the framework to fit our new scenario, thereby ensuring that nothing is bent just to make things fit.
Custom methods are simple conceptually (RPC-style calls are common), but details matter: when is it safe to use them, are standard methods truly unsuitable, can custom calls be parameterized, etc. Before diving into mechanics, ask: do we really need custom methods?
Why not just standard methods?#
While the standard methods we learned about in chapter 7 are often sufficient to do pretty much anything with an API, there are times where they feel not quite right. In these cases, it often feels as though we’re following the letter of the law, but not the spirit of that law: having standard methods act sufficiently different from expectations, leading to surprise, and therefore not a very good API.
A concrete example: state changes (e.g., an email going from draft
→ sent
). Updating a state field via the standard update
method is conceptually different from changing a scalar field like subject
. It conflates “set a stored value” with “perform a state transition,” which is confusing and surprising.
Another issue is side effects: standard methods should do only what their name implies (create should create, update should update). State transitions often require external actions (sending mail via SMTP). Using update
to trigger these external systems mixes storage updates with external side effects and chains multiple dependencies (storage + external service), making the update operation potentially long-running and failure-prone.
Rearranging the resource model (e.g., EmailDraft
vs Email
) is possible, but not always the best trade-off. The goal is a simple, expressive, predictable API — sometimes it’s better to adapt the framework (add custom methods) rather than contort resources or standard methods.
Overview of custom methods#
Custom methods are nothing more than API calls that fall outside the scope of a standard method and therefore aren’t subject to the strict requirements that we impose on standard methods. They might look a bit different, as we’ll see in the next section, but from a purely technical standpoint there is really nothing special about custom methods.
- Standard methods bring many useful guarantees but also constraints.
- Custom methods have few constraints: they can do what best fits the scenario (including side effects).
- Downside: clients cannot assume as many guarantees about custom methods as they can about standard ones. For that reason, define and apply consistent rules/precedents for custom methods across the API.
Questions that need careful design: how to supply extra context/parameters for state changes, should the method target a single resource vs a collection, or what if no state is involved at all?
Implementation details#
- HTTP method: almost always
POST
.GET
could be used if the custom method is idempotent and safe, DELETE is rare. AvoidPATCH
for custom methods. - Path format: resource path is the same, but use a colon (
:
) to separate the resource/collection target from the action to avoid ambiguity. Example:POST /rockets/1234567:launch
(rather than/rockets/1234567/launch
). - RPC naming: follow verb+noun (e.g.,
LaunchRocket
,ArchiveDocument
), avoid prepositions likefor
orwith
. Custom methods are not a substitute for parameterizing standard methods.
Side effects#
Perhaps the largest difference between custom methods and standard methods is the acceptance of side effects. … Custom methods are exactly the right place to have actions with side effects.
- Standard methods should avoid side effects; custom methods may intentionally perform them (send email, trigger background ops, update multiple resources).
- Example flow:
CreateEmail
creates a draft (pure DB write).SendEmail
(custom) handles talking to SMTP, and only after success transitions the resource tosent
. This keeps standard methods pure and confines side effects to the custom method. - Custom methods can target resources or collections — design choice depends on scope.
Resources vs. collections#
Standard methods are either resource-targeted (single resource) or collection-targeted (parent collection). Custom methods must likewise choose their target.
Rule of thumb:
- If multiple resources from the same collection are involved → target the collection:
POST /users/1/emails:export
orPOST /users/1/emails:deleteMany
. - If the focus is the parent resource (e.g., export all user information, including emails) → target the parent resource:
POST /users/1:export
. - If operating across multiple different parents, use a wildcard parent and list IDs in the body:
POST /users/-/emails:archive
(where-
denotes a wildcard and the body contains the concrete email IDs).
- If multiple resources from the same collection are involved → target the collection:
Stateless custom methods#
- A stateless method: not attached to any resource/collection and does not persist input data; it processes data on the fly and returns a result. Useful for privacy-sensitive scenarios or regulations (e.g., GDPR) that restrict storage.
- Purely stateless methods are relatively rare because many APIs need billing, permissions, or quota context. Often a parent resource (project, billing account, organization) acts as a container and the otherwise-stateless method is attached to that parent.
- Example: translation could be stateless, but you might attach it to a
project
resource to track billing.
Caveat: anticipate future needs. Today a single translation model may be fine, but tomorrow multiple ML models, custom glossaries, or user-deployed models may be required. Pure statelessness makes evolving to such scenarios harder. Consider modeling ML models or configurations as resources and attach the custom method to those resources — the method can remain stateless in that it doesn’t persist each call’s payload, but it gains configurability.
Final API example (email)#
For illustration, the chapter shows an Email API with standard methods for managing Email
resources plus several custom methods:
abstract class EmailApi {
static version = "v1";
static title = "Email API";
// All the normal standard methods (e.g., CreateEmail, DeleteEmail, etc.) would go here.
@post("/{id=users/*/emails/*}:send")
SendEmail(req: SendEmailRequest): Email;
@post("/{id=users/*/emails/*}:unsend")
UnsendEmail(req: UnsendEmailRequest): Email;
@post("/{id=users/*/emails/*}:undelete")
UndeleteEmail(req: UndeleteEmailRequest): Email;
// The custom methods to send an email message would transition the
// email to a sending state, delay for a few seconds, connect to the SMTP
// service, and return the result.
// The unsend method would allow a send operation to abort during the
// delay introduced by the send method.
// The undelete method would update the Email.deleted property, performing the
// inverse of the standard delete method (see chapter 25 for more information on soft deletion).
@post("/{parent=users/*}/emails:export")
ExportEmails(req: ExportEmailsRequest): ExportEmailsResponse;
@post("/emailAddress:validate")
ValidateEmailAddress(req: ValidateEmailAddressRequest): ValidateEmailAddressResponse;
// This stateless email address validation method is clearly free (as it’s not tied to any parent).
}
interface Email {
id: string;
subject: string;
content: string;
state: string;
deleted: boolean;
// ...
// The export method would take all email data and push it to a remote storage
// location (see chapter 23 for more information on importing and exporting).
}
Trade-offs#
- Custom methods fill functionality gaps left by standard methods and resource design, providing a pragmatic middle ground.
- However, their existence is in tension with pure RESTful design: anything a custom method does could be modeled via standard methods with a different resource design.
- Danger: overuse or misuse. Custom methods are sometimes abused to paper over a poor resource model. If many common actions that should be standard are implemented as custom methods, that indicates bad resource layout.
- Before adding a custom method, ensure it’s not simply duct-taping over a wrong resource design.
Questions (from chapter end)#
- What if creating a resource requires some sort of side effect? Should this be a custom method instead? Why or why not?
- When should custom methods target a collection? What about a parent resource?
- Why is it dangerous to rely exclusively on stateless custom methods?
Condensed answers (summary / best-practice form):
- Yes — use a custom method when the side effect breaks the semantic guarantees of standard methods (e.g., idempotence or purity). Standard methods are expected to be predictable and limited; if creation implies external side effects that violate those expectations, model it as a custom action.
- Target a collection when the operation acts on multiple resources from the same collection. Target the parent resource when the primary focus is the parent (e.g., exporting a user’s full profile including emails). For cross-parent sets, use a wildcard parent (
-
) and specify concrete IDs in the request body. - Because of future extensibility and configurability: pure statelessness makes it hard to support future needs (different ML models, per-user configs, billing/permissions). Tying functionality to resources where appropriate (e.g., model or project resources) preserves flexibility.
Final short summary (bullets)#
- Custom methods typically use
POST
and use:
to separate target and action (e.g.,/users/1/emails:export
). - Custom methods are allowed to have side effects; standard methods should not. Use custom methods sparingly and document them well.
- Prefer collection-targeted custom methods when operating on multiple items; prefer parent-targeted when the parent is the focus.
- Stateless custom methods are useful for privacy-sensitive, on-the-fly computation, but beware future needs — consider attaching behavior to configurable resources when extensibility is required.
- Don’t let custom methods mask a bad resource design. If an action rightfully belongs to a standard method given a correct resource model, implement it as such.