Orchestr

Actions

Understanding the basics of Orchestr actions

Orchestr actions are the core building blocks for interacting with external services. They can be used to subscribe to newsletters, create orders, or any other action that requires interaction with an external service.

Tokens

An action-token is a string that uniquely identifies an action. Additionally, it contains type-metadata for the action like the input and output-types. In practice this makes the action-token a contract between the frontend and the backend for executing a server-side request.

Each action-token can be implemented in an app-package by an Action Handler and can be used in frontend components using the action-composables (See Frontend Usage).

Canonical Tokens

A canonical action-token is a pre-defined token that is part of the @laioutr-core/canonical-types package. This package contains action-tokens for common actions like subscribing to a newsletter, creating an order, or retrieving customer orders.

You can find a list of canonical action-tokens on the Canonical Actions page.

Custom Tokens

Sometimes you might want to create a custom action-token that is not part of the canonical-types package. This can be done by creating a new action-token and implementing an Action Handler for it.

Input and output of an action are defined using the zod library. This allows for type-safety and validation of the input and output data. Both input and output are optional and will default to undefined.

shared/tokens/newsletter/custom.action.ts
import { 
z
} from 'zod/v4';
import {
defineActionToken
} from '@laioutr-core/canonical-types/orchestr';
// It is recommended to use a namespace like `my-package/` to avoid conflicts with other packages. export const
CustomAction
=
defineActionToken
('my-package/newsletter/custom-action', {
input
:
z
.
object
({
email
:
z
.
string
(),
}),
output
:
z
.
object
({
status
:
z
.
enum
(['success', 'error']),
}), });

Action Handler

Orchestr actions are defined using the defineOrchestr.actionHandler method. This method takes an action-token for type-safety and a handler function that will be called when the action is executed. The handler function will receive the input-data of the action-token and must return the output-data of the action-token.

server/orchestr/newsletter/subscribe.ts
import { 
SubscribeAction
} from '@laioutr-core/canonical-types/newsletter';
// Export the return-value as default to register it automatically export default
defineOrchestr
.
actionHandler
(
SubscribeAction
, async ({
input
}) => {
await
subscribeToNewsletter
(
input
.
email
);
return {
status
: 'success' as
const
};
});

Technically, each registered action acts as an http POST handler on the server. The path is computed, using the action-token name. E.g. ecommerce/auth/register will be available at POST /api/orchestr/action/ecommerce/auth/register.

The action response is encoded as a turbo-stream response, which is a superset of JSON. This means that any data-type supported by turbo-stream can be returned. This includes regular objects and arrays but also Dates, Maps, Sets, etc.

Additionally, the client will add the clientEnv to the request. This is an object which contains information about the client environment (like his locale or currency) to the action-handler.

server/orchestr/newsletter/subscribe.ts
import { 
AuthRegisterAction
} from '@laioutr-core/canonical-types/ecommerce';
// Alternatively, you can use the shortcut `defineActionHandler` export default
defineActionHandler
(
AuthRegisterAction
, async ({
clientEnv
}) => {
const
userLanguage
=
getLanguageByLocale
(
clientEnv
.
locale
);
// ... });

Middleware

Middleware is a way to intercept and modify the input, output or context of an action. It is a way to add additional functionality to an action without having to modify the action-handler.

See Middleware for more information.

Error Handling

If executing an action on the server fails, an error should be thrown. You can either just throw a generic Error object or use one of the error-classes from the @laioutr-core/orchestr/* package.

server/orchestr/cart/add-items.ts
import { 
CartAddItemsAction
,
ProductNotFoundError
} from '@laioutr-core/canonical-types/ecommerce';
export default
defineActionHandler
(
CartAddItemsAction
, async ({
input
}) => {
throw new
ProductNotFoundError
('product-123');
});

For custom actions, you can also create your own error-classes using the ebec or supplementary @ebec/http package.

import { 
CartAddItemsAction
} from '@laioutr-core/canonical-types/ecommerce';
import {
PreconditionFailedError
} from '@ebec/http';
export default
defineActionHandler
(
CartAddItemsAction
, async ({
input
}) => {
throw new
CustomNotFoundError
('product-123');
// => 404 Not Found throw new
CustomGeneralError
();
// => 500 Internal Server Error throw new
PreconditionFailedError
('Custom message');
// => 412 Precondition Failed throw new
Error
('Regular error object');
// => 500 Internal Server Error });

Frontend Usage

Actions can be called from the frontend using the useQueryAction or useMutationAction composables. These use pinia-colada under the hood.

Query

Uses pinia-colada's useQuery method. You will have access to typed data through the data property. All other properties from the pinia-colada's useQuery method are available as well. You can pass a second argument to pass typed input to the action.

Queries are executed immediately when the component is mounted or when the input changes.

app/get-customer-orders.ts
import { 
AddressGetAllAction
} from '@laioutr-core/canonical-types/ecommerce';
// Hover over `data` to see the type of the data const {
data
,
isLoading
} =
useQueryAction
(
AddressGetAllAction
);

Mutation

Uses pinia-colada's useMutation method.

Mutations are preferred over queries when you want to execute an action on an event like a button click.

app/components/newsletter-form.vue
<script setup lang="ts">
import { 
SubscribeAction
} from '@laioutr-core/canonical-types/newsletter';
const {
mutate
:
subscribeNewsletter
,
isLoading
} =
useMutationAction
(
SubscribeAction
);
const
email
=
ref
('')
</script> <template> <
form
@
submit
.prevent="
subscribeNewsletter
({
email
})">
<
input
v-model="
email
" />
<
button
:disabled
="
isLoading
">
Subscribe to newsletter </
button
>
</
form
>
</template>

Other Composables

fetchAction

The fetchAction function is the bare-bone function that makes a post-request to orchestr to execute an action. It returns the decoded response or throws on error. There is no caching or other features built-in.

const 
response
= await
fetchAction
(
SubscribeAction
, {
email
: '[email protected]'
});

useFetchAction

The useFetchAction composable is a wrapper around the fetchAction function. It provides the same functionality as the fetchAction function but uses the useAsyncData composable under the hood in order to fetch data only once when running on the server.

const { 
data
,
error
} = await
useFetchAction
(
SubscribeAction
, {
email
: '[email protected]'
});

Action Flow

The following diagram shows how an action flows through the system: