Handling External API Response Specifications
Learn how libmodulor's externalSpecResponse enables use cases to return standards-compliant API schemas, such as OAuth 2.0 responses.
In libmodulor, the output of every use case is the same.
It satisfies the following interface (simplified for the article) :
export interface UCOutput<OPI0, OPI1> {
parts: {
_0: {
items: OPI0[],
total: number,
};
_1?: {
items: OPI1[],
total: number,
};
};
}
So a typical response looks like this :
interface OPI0 extends UCOPIBase {
firstname: PersonFirstname;
}
const output: UCOutput<OPI0> = {
parts: {
_0: {
items: [
{
id: 'b4816428-2b32-4dd5-854e-0ac8725877e2',
firstname: 'Toto',
},
{
id: '08b7a197-0542-4167-a951-961d034e2a43',
firstname: 'Titi',
}
],
total: 2,
};
};
}
Although a little bit verbose, such an output gives a unified response schema that works for any use case. Therefore, clients always know how to read and process them.
The external specs problem
The problem with such a schema is that sometimes, it simply does not work. Sometimes, an endpoint is expected to respond with a very specific schema that satisfies a standard.
Let's take OAuth2 as an example. Let's say we want to implement a use case that issues a new token to the user after the "OAuth" dance.
The spec explicitly states that the response must look like this :
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"token_type": "Bearer",
}
This clearly does not fit our "generic" schema described above.
Use case def extension at the rescue
Since libmodulor v0.34.0 it is possible to define the externalSpecResponse so use cases behave exactly as we want them to.
export interface OAuthGetTokenInput extends UCInput {
client_id: UCInputFieldValue<UUID>;
client_secret: UCInputFieldValue<ApiKey>;
code: UCInputFieldValue<ApiKey>;
code_verifier: UCInputFieldValue<ApiKey>;
grant_type: UCInputFieldValue<OAuthGrantType>;
redirect_uri: UCInputFieldValue<URL>;
}
export interface OAuthGetTokenOPI0 extends UCOPIBase {
access_token: ApiKey;
expires_in: UIntDuration;
refresh_token: ApiKey | null;
token_type: OAuthTokenType;
}
export const OAuthGetTokenUCD: UCDef<OAuthGetTokenInput, OAuthGetTokenOPI0> = {
ext: {
http: {
externalSpecResponse:
'https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4',
mountAt: '/api/v1/oauth/token',
},
},
// ... rest omitted
}
Note how we use snake_case for the properties names. In libmodulor, the convention is camelCase and PascalCase, but when a spec requires a specific schema, this is acceptable.
By defining ext.http.externalSpecResponse, we explicitly tell the server that mounts the use case to treat it like a use case respecting an external spec.
Therefore, the output will be sent as is : a single flat OAuthGetTokenOPI0 instead of the parts, items, total thing.
This way, clients understanding the OAuth spec (e.g. MCP) are able to invoke this use case without any additional boilerplate to remap the output.
It applies to OAuth, but also to any external caller that expects a specific and strict schema as a response.