Command
A Command is a function that takes a single input and returns a single output. It can be called over HTTP via RPC or (optionally) REST.
Create a new Command
Import the command
primitive from @eventual/core
and create an instance.
import { command } from "@eventual/core";
export const hello = command("hello", async (name: string) => {
return `hello ${name}`;
});
It's recommended to use Zod to define at least your input
schema so that your Command is protected from receiving invalid data. See the Input Schema docs for more information.
Call a Command
Use the ServiceClient to interact with your service's Commands without code generation.
import type * as MyService from "@my-service/service";
const client = new ServiceClient<typeof MyService>({
serviceUrl: process.env.SERVICE_URL,
});
const response = await client.hello("my name");
Provide an input
schema with Zod
Zod can be used to define a schema to protect your API. For example, we can re-write the previous hello
command using zod to define the input type:
import { z } from "zod";
export const hello = command(
"hello",
{
input: z.string(),
},
async (name) => {
return `hello ${name}`;
}
);
Eventual will automatically generate Open API 3.0 schemas and attach it to your API gateway. If the command is called with an input payload that does not match the schema, a 400 Bad Request
will be returned without ever hitting your Command's Lambda Function.
Zod is a TypeScript library for defining schemas and validating data.
Provide an output
schema with Zod
Similarly, you can specify the output
schema also using Zod.
import { z } from "zod";
export const hello = command(
"hello",
{
output: z.string(),
},
async (name: string) => {
return `hello ${name}`;
}
);
Middleware
Commands can be integrated with middleware chains that perform functions such as validating requests, setting headers, authorizing and fetching user information.
To create a Command with middleware, use the api.use
utility to first create a middleware chain, and then finally created the command.
export const hello = api
.use(cors)
.use(authorized)
.command("hello", async (name: string, { user }) => {
// etc.
});
See the Middleware documentation for more information.
HttpError
The HttpError
class in @eventual/core
can be thrown from a Command to fail the request with a status code
, message
and optional data
property.
Ex. to fail a request with 404
- Not Found:
throw new HttpError({
code: 404,
message: "Item was not found",
});
Remote Procedure Call (RPC) API Route
All commands attach a RPC route to your Service's API Gateway with the following format:
POST /rpc/${commandName}
It accepts a JSON payload body for the Command's input and returns the output as a JSON-encoded body.
See the RESTful API docs for information on how to also expose a Command as a REST APi.
Expose a Command as a RESTful API
Commands are built around RPC (input and output) for simplicity, but there are cases where exposing a RESTful API design is preferable to a RPC one. Commands can be configured as RESTful endpoints using the method
, path
and params
properties.
For example, here we have a Command that sets a key-value pair in a database. By default, it is available to be called via POST /_rpc/setKey
.
export const setKey = command(
"setKey",
{
input: z.object({
key: z.string(),
value: z.string()
}),
},
async (({key, value})) => {
await db.put(key, value)
}
);
To adapt it to a RESTful interface, we can specify method
, path
and params
:
Your input schema must be an object with properties.
export const setKey = command(
"setKey",
{
// available
method: "POST",
// the REST path endpoint
// the key property comes from the path
path: "/:key",
params: {
// the value comes from the body
value: "body"
},
input: z.object({
key: z.string(),
value: z.string()
}),
},
async (({key, value})) => {
await db.put(key, value)
}
);
This can now be called with the following API request:
POST /my-key
my-value
Which will be mapped to the following payload as input to the setKey
command
{
key: "my-key",
value: "my-value",
}