Request middleware
A request middleware in Alette Signal is a function that instructs the core system on how to react to request lifecycle stages.
Middleware categories
Alette Signal has 5 middleware categories:
Creation
- middleware preparing request data before execution - body, path, query params, etc., are all configured here. For example,input()
andoutput()
are creational middleware.Behaviour
- middleware that modify how requests are going to be executed.Execution
- middleware that execute requests by calling REST API endpoints, etc.Transformation
- middleware transforming request response and errors.Inspection
- middleware hooking into request lifecycle and performing side effects without modifying response and errors.
TIP
For full middleware list consult Alette Signal middleware reference.
Middleware organization
Middleware organization refers to middleware ability to automatically modify and sort other middleware before they are initialized.
This behaviour can be seen when adding 2 input()
middleware to the same request:
const query1 = myQuery.with(
input(as<{ willBeOverridden: true }>()),
input(as<string>())
)
// The "args" prop will expect a string here,
// not "{ willBeOverridden: true }"
await query1.execute({ args: 'overridden' })
INFO
- The
input()
middleware is configured to remove all previousinput()
middleware from the chain. This is also true for other middleware likeoutput()
,runOnMount()
,debounce()
, etc. - This behaviour is reflected in blueprint TypeScript types.
Middleware priority
Middleware priority refers to middleware order in request blueprint configuration.
This behaviour can be seen with the retryWhen()
and mapError()
middleware:
const query1 = myQuery.with(
input(as<string>()),
mapError((error) => new MyCustomError()),
// The "error" property inside "retryWhen()" is
// always of "RequestFailedError" type, not "MyCustomError".
retryWhen(async ({ error }) => {
return true;
})
)
Even though mapError()
is placed before retryWhen()
, their order will be reversed before initialization:
// After
myQuery.with(
input(as<string>()),
retryWhen(async ({ error }) => {
return true;
}),
mapError((error) => new MyCustomError()),
)
INFO
Middleware sorting by priority is reflected in blueprint TypeScript types.
TIP
- If you are not sure about the middleware order of a request blueprint, always look at its TypeScript types.
- Most middleware expose previously set request data, allowing for type verification:
const query1 = myQuery.with(
path('/alette'),
path(
{ /* other request data */ },
// prevPath is of "/alette" type here
prevPath => `${prevPath}/signal`
),
)
Middleware composition
Middleware composition is a feature of Alette Signal allowing same type middleware chaining in request blueprints.
Let's compose multiple map()
middleware to transform our server response:
const query1 = myQuery.with(
output(as<string>()),
path('/alette'),
map((response) => `${response}/map1`),
map((response) => `${response}/map2`),
map((response) => `${response}/map3`),
)
// The "response" type will be "${string}/map1/map2/map3"
const response = await query1.execute({ args: 'hey' })
Middleware composition also works across blueprints:
const query1 = myQuery.with(
output(as<string>()),
path('/alette'),
map((response) => `${response}/map1`),
)
const query2 = query1.with(
map((response) => `${response}/map2`),
)
const query3 = query2.with(
map((response) => `${response}/map3`),
)
// The "response1" type will be "${string}/map1"
const response1 = await query1.execute({ args: 'hey' })
// The "response2" type will be "${string}/map1/map2"
const response2 = await query2.execute({ args: 'hey' })
// The "response3" type will be "${string}/map1/map2/map3"
const response3 = await query3.execute({ args: 'hey' })
TIP
Notice how query1
, query2
and query3
are executed independently of each other, each with their own middleware list. Even through query3
uses query2
as a foundation, they never collide with one another. This is achieved via Alette Signal request behaviour inheritance.
Middleware cascading
Middleware cascading is a middleware behaviour that overrides previous request data set by other middleware.
This behaviour can be seen when adding multiple headers()
middleware:
const query1 = myQuery.with(
output(as<string>()),
headers({ 'header1': 'hi' }),
headers({ 'header2': 'hello' }),
)
// The request will be executed with
// { 'header2': 'hello' } object as headers.
await query1.execute({ args: 'hey' })
To preserve the data provided by previous cascading middleware, pass a callback as an argument to merge request data manually:
const query1 = myQuery.with(
output(as<string>()),
headers({ 'header1': 'hi' }),
headers((_, prevHeaders) => ({ ...prevHeaders, 'header2': 'hello' })),
)
// Now the request will be executed with
// { 'header1': 'hi', 'header2': 'hello' } object as headers.
await query1.execute({ args: 'hey' })
Manual request data merging also works across blueprints:
const query1 = myQuery.with(
output(as<string>()),
headers({ 'header1': 'hi' }),
)
const query2 = query1.with(
headers((_, prevHeaders) => ({ ...prevHeaders, 'header2': 'hello' })),
)
// The request will be executed with
// { 'header1': 'hi' } object as headers.
await query1.execute({ args: 'hey' })
// The request will be executed with
// { 'header1': 'hi', 'header2': 'hello' } object as headers.
await query2.execute({ args: 'hey' })