@tnezdev/actions
Patterns to express business logic in a way that encourages using dependency injection in order to interact with side-effects to simplify unit testing.
Actions are guaranteed to return a response in a consistent envelope, { ok: true, data: Data } for the happy path and { ok: false, error: Error } if an issue occurs. This allows for simplified error handling in the event of the sad path.
Installation
- npm:
npm i @tnezdev/actions
- pnpm:
pnpm add @tnezdev/actions
- yarn:
yarn add @tnezdev/actions
Usage
You can express your core business logic as actions. You may do this in files that look something like this toy example:
import { createAction } from "@tnezdev/actions";
import type { ActionHandler } from "@tnezdev/actions";
import type { WeatherClient } from "@/clients/weather";
export type GetTemperatureContext = {
client: WeatherClient;
scale?: "celsius" | "fahrenheit";
};
export type GetTemperatureInput = { zipcode: string };
export type GetTemperatureOutput = { temperature: number };
const handler: ActionHandler<
GetTemperatureContext,
GetTemperatureInput,
GetTemperatureOutput
> = async (ctx, input) => {
const { client, scale } = ctx;
const { zipcode } = input;
ctx.logger.info("You can emit logs from inside the action");
const { temperature } = await client.getTempearture(zipcode, { scale });
return { temperature };
};
export const GetTemperatureAction = createAction("GetTemperature", handler);
This can then be used in your application by something that is triggered by a user or the system. This may often be something like an API route or a handler invoked from a CLI. For this example, let's pretend we are using from inside an API route in a NextJS application that lives at https://api.domain.com/weather/[zipcode].
import * as z from "zod";
import { GetTemperatureAction } from "@/actions/weather";
import { WeatherClient } from "@/clients/weather";
import { NextRequest, NextResponse } from "next/server";
const getTempeartureAction = new GetTemperatureAction({
client: new WeatherClient(),
scale: "fahrenheit",
});
const RequestContext = z.object({
params: z.object({
zipcode: z.string(),
}),
});
export async function GET(request: , requestContext: unknown) {
const { zipcode } = RequestContext.parse(requestContext);
const { ok, data, error } = await getTempeartureAction.run({
zipcode: "12345",
});
return ok
? NextResponse.json({ data })
: NextResponse.next({ status: 500 }).json({ error });
}
When run, this will produce the following logs:
[GetTemparature:{correlation-id}] Action Started (input: {"zipcode":"12345"})
[GetTemperature:{correlation-id}] You can emit logs from inside the action
[GetTempearture:{correlation-id}] Action Completed (data: {"temperature":"75˚F"})
And the result returned from the action will be:
{ ok: true, data: { temperature: 72 } }
You can see a more complete example of real-world usage here: /src/examples/README.md