Actions
We'll explain what Actions are and how to use them. If you're looking for a detailed API specification, skip ahead to the API Reference.
Actions are quite similar to Queries, but with a key distinction: Actions are designed to modify and add data, while Queries are solely for reading data. Examples of Actions include adding a comment to a blog post, liking a video, or updating a product's price.
Actions and Queries work together to keep data caches up-to-date.
Actions are almost identical to Queries in terms of their API. Therefore, if you're already familiar with Queries, you might find reading the entire guide repetitive.
We instead recommend skipping ahead and only reading the differences between Queries and Actions, and consulting the API Reference as needed.
Working with Actions
Actions are declared in Wasp and implemented in NodeJS. Wasp runs Actions within the server's context, but it also generates code that allows you to call them from anywhere in your code (either client or server) using the same interface.
This means you don't have to worry about building an HTTP API for the Action, managing server-side request handling, or even dealing with client-side response handling and caching. Instead, just focus on developing the business logic inside your Action, and let Wasp handle the rest!
To create an Action, you need to:
- Declare the Action in Wasp using the
actionspec. - Implement the Action's NodeJS functionality.
Once these two steps are completed, you can use the Action from anywhere in your code.
Specifying Actions
To create an Action in Wasp, we begin with an action spec. Let's declare two Actions - one for creating a task, and another for marking tasks as done:
import { action, app } from "@wasp/spec"
import { createTask, markTaskAsDone } from "./src/actions" with { type: "ref" }
export default app({
// ...
spec: [
action(createTask),
action(markTaskAsDone),
],
})
If you want to know about all supported options for the action spec, take a look at the API Reference.
When main.wasp.ts needs to point to your code, it uses imports like this:
import { MainPage } from "./src/MainPage" with { type: "ref" }
Notice the with { type: "ref" } part at the end of the import statement. This tells Wasp to treat the import as a reference to your app's code, without running the imported code. For more details and examples, see reference imports.
You might have noticed that we told Wasp to import Action implementations that don't yet exist. Don't worry about that for now. We'll write the implementations imported from actions.ts in the next section.
It's a good idea to start with the high-level concept (the Action spec in the Wasp file) and only then deal with the implementation details (the Action's implementation in JavaScript).
After declaring a Wasp Action, Wasp derives the Action's name from the function you pass to action. For example, action(markTaskAsDone) creates an Action named markTaskAsDone.
Two important things then happen:
-
Wasp generates a server-side NodeJS function with the Action's name.
-
Wasp generates a client-side JavaScript function with the Action's name (e.g.,
markTaskAsDone). This function takes at most one argument: any serializable data you wish to use inside the Action (usually an object). Wasp will send this data over the network and pass it into the Action's implementation as its first positional argument (more on this when we look at the implementations). Such an abstraction works thanks to an HTTP API route handler Wasp generates on the server, which calls the Action's NodeJS implementation under the hood.
Generating these two functions ensures a similar calling interface across the entire app (both client and server).
Implementing Actions in Node
Now that we've declared the Action, what remains is to implement it. We've instructed Wasp to look for the Actions' implementations in the file src/actions.ts, so that's where we should export them from.
Here's how you might implement the previously declared Actions createTask and markTaskAsDone:
- JavaScript
- TypeScript
// our "database"
let nextId = 4
const tasks = [
{ id: 1, description: "Buy some eggs", isDone: true },
{ id: 2, description: "Make an omelette", isDone: false },
{ id: 3, description: "Eat breakfast", isDone: false },
]
// You don't need to use the arguments if you don't need them
export const createTask = (args) => {
const newTask = {
id: nextId,
isDone: false,
description: args.description,
}
nextId += 1
tasks.push(newTask)
return newTask
}
// The 'args' object is something sent by the caller (most often from the client)
export const markTaskAsDone = (args) => {
const task = tasks.find((task) => task.id === args.id)
if (!task) {
// We'll show how to properly handle such errors later
return
}
task.isDone = true
}
Wasp uses superjson under the hood. This means you're not limited to only sending and receiving JSON payloads.
Wasp will automatically handle the serialization and deserialization
for all the data types that superjson supports
(like biging, Date, Map, Set, etc.), and for
Prisma.Decimal.
As long as you're annotating your Operations with the correct automatically generated types, TypeScript ensures your payloads are valid (i.e., Wasp knows how to serialize and deserialize them).
import { type CreateTask, type MarkTaskAsDone } from "wasp/server/operations"
type Task = {
id: number
description: string
isDone: boolean
}
// our "database"
let nextId = 4
const tasks = [
{ id: 1, description: "Buy some eggs", isDone: true },
{ id: 2, description: "Make an omelette", isDone: false },
{ id: 3, description: "Eat breakfast", isDone: false },
]
// You don't need to use the arguments if you don't need them
export const createTask: CreateTask<Pick<Task, "description">, Task> = (
args
) => {
const newTask = {
id: nextId,
isDone: false,
description: args.description,
}
nextId += 1
tasks.push(newTask)
return newTask
}
// The 'args' object is something sent by the caller (most often from the client)
export const markTaskAsDone: MarkTaskAsDone<Pick<Task, "id">, void> = (
args
) => {
const task = tasks.find((task) => task.id === args.id)
if (!task) {
// We'll show how to properly handle such errors later
return
}
task.isDone = true
}
Wasp uses superjson under the hood. This means you're not limited to only sending and receiving JSON payloads.
Wasp will automatically handle the serialization and deserialization
for all the data types that superjson supports
(like biging, Date, Map, Set, etc.), and for
Prisma.Decimal.
As long as you're annotating your Operations with the correct automatically generated types, TypeScript ensures your payloads are valid (i.e., Wasp knows how to serialize and deserialize them).
Type support for Actions
Wasp automatically generates the types CreateTask and MarkTaskAsDone based on the specs in your Wasp file:
CreateTaskis a generic type that Wasp automatically generated based on the Action spec forcreateTask.MarkTaskAsDoneis a generic type that Wasp automatically generated based on the Action spec formarkTaskAsDone.
Use these types to type the Action's implementation. It's optional but very helpful since doing so properly types the Action's context.
In this case, TypeScript will know the context.entities object must include the Task entity.
TypeScript also knows whether the context object includes user information (it depends on whether your Action uses auth).
The generated types are generic and accept two optional type arguments: Input and Output.
Input- The argument (the payload) received by the Action function.Output- The Action function's return type.
Use these type arguments to type the Action's inputs and outputs. The above code says that the Action On the other hand, the Action If you don't care about typing the Action's inputs and outputs, you can omit both type arguments.
TypeScript will then infer the most general types ( Specifying Explanation for the example above
createTask expects an object with the new task's description (its input type is Pick<Task, 'description'>) and returns the new task (its output type is Task).markTaskAsDone expects an object of type Pick<Task, 'id'>. This type is derived from the Task entity type.never for the input and Payload, any serializable value, for the output).Input or Output is completely optional, but we highly recommended it. Doing so gives you:
Read more about type support for implementing Actions in the API Reference.
If you don't want to explicitly type the Action's return value, the satisfies keyword tells TypeScript to infer it automatically:
const createFoo = (async (_args, context) => {
const foo = await context.entities.Foo.create({ data: {} })
return {
newFoo: foo,
message: "Here's your foo!",
returnedAt: new Date(),
}
}) satisfies CreateFoo
From the snippet above, TypeScript knows:
- The correct type for
context. - The Action's return type is
{ newFoo: Foo, message: string, returnedAt: Date }.
If you don't need the context, you can skip specifying the Action's type (and arguments):
const createFoo = () => ({ name: "Foo", date: new Date() })
For a detailed explanation of the Action definition API (more precisely, its arguments and return values), check the API Reference.
Using Actions
Using Actions on the client
To call an Action on the client, you can import it from wasp/client/operations and call it directly.
The usage doesn't depend on whether the Action is authenticated or not. Wasp authenticates the logged-in user in the background.
- JavaScript
- TypeScript
import { createTask, markTaskAsDone } from "wasp/client/operations"
// ...
const newTask = await createTask({ description: "Learn TypeScript" })
await markTaskAsDone({ id: 1 })
import { createTask, markTaskAsDone } from "wasp/client/operations"
// TypeScript automatically infers the return values and type-checks
// the payloads.
const newTask = await createTask({ description: "Keep learning TypeScript" })
await markTaskAsDone({ id: 1 })
Wasp supports automatic full-stack type safety. You only need to specify the Action's type in its server-side definition, and the client code will automatically know its API payload types.
When using Actions on the client, you'll most likely want to use them inside a component:
- JavaScript
- TypeScript
import React from "react"
import { useQuery, getTask, markTaskAsDone } from "wasp/client/operations"
export const TaskPage = ({ id }) => {
const { data: task } = useQuery(getTask, { id })
if (!task) {
return <h1>"Loading"</h1>
}
const { description, isDone } = task
return (
<div>
<p>
<strong>Description: </strong>
{description}
</p>
<p>
<strong>Is done: </strong>
{isDone ? "Yes" : "No"}
</p>
{isDone || (
<button onClick={() => markTaskAsDone({ id })}>Mark as done.</button>
)}
</div>
)
}
import React from "react"
import { useQuery, getTask, markTaskAsDone } from "wasp/client/operations"
export const TaskPage = ({ id }: { id: number }) => {
const { data: task } = useQuery(getTask, { id })
if (!task) {
return <h1>"Loading"</h1>
}
const { description, isDone } = task
return (
<div>
<p>
<strong>Description: </strong>
{description}
</p>
<p>
<strong>Is done: </strong>
{isDone ? "Yes" : "No"}
</p>
{isDone || (
<button onClick={() => markTaskAsDone({ id })}>Mark as done.</button>
)}
</div>
)
}
Since Actions don't require reactivity, they are safe to use inside components without a hook. Still, Wasp comes with the useAction hook you can use to enhance Actions. Read all about it in the API Reference.
Using Actions on the server
Calling an Action on the server is similar to calling it on the client.
Here's what you have to do differently:
- Import Actions from
wasp/server/operationsinstead ofwasp/client/operations. - Make sure you pass in a context object with the user to authenticated Actions.
- JavaScript
- TypeScript
import { createTask, markTaskAsDone } from "wasp/server/operations"
const user = // Get an AuthUser object, e.g., from context.user
const newTask = await createTask(
{ description: "Learn TypeScript" },
{ user },
)
await markTaskAsDone({ id: 1 }, { user })
import { createTask, markTaskAsDone } from "wasp/server/operations"
const user = // Get an AuthUser object, e.g., from context.user
// TypeScript automatically infers the return values and type-checks
// the payloads.
const newTask = await createTask(
{ description: "Keep learning TypeScript" },
{ user },
)
await markTaskAsDone({ id: 1 }, { user })
Error Handling
For security reasons, Wasp hides the details of exceptions thrown in the Action's NodeJS implementation: by default, the client gets a generic response with the HTTP status code 500. In development, the response also includes the original message and stack trace.
Hiding error details by default helps against accidentally leaking possibly sensitive information over the network.
If you do want to pass additional error information to the client, you can construct and throw an appropriate HttpError in your implementation:
- JavaScript
- TypeScript
import { HttpError } from "wasp/server"
export const createTask = async (args, context) => {
throw new HttpError(
403, // status code
"You can't do this!", // message
{ foo: "bar" } // data
)
}
import { type CreateTask } from "wasp/server/operations"
import { HttpError } from "wasp/server"
export const createTask: CreateTask = async (args, context) => {
throw new HttpError(
403, // status code
"You can't do this!", // message
{ foo: "bar" } // data
)
}
To also set response headers, e.g. Retry-After, pass them in the options argument:
throw new HttpError(429, "Too many requests", undefined, {
headers: { "Retry-After": "60" },
})
Using Entities in Actions
In most cases, resources used in Actions will be Entities.
To use an Entity in your Action, add it to the action spec in Wasp:
import { action, app } from "@wasp/spec"
import { createTask, markTaskAsDone } from "./src/actions" with { type: "ref" }
export default app({
// ...
spec: [
action(createTask, { entities: ["Task"] }),
action(markTaskAsDone, { entities: ["Task"] }),
],
})
Wasp will inject the specified Entity into the Action's context argument, giving you access to the Entity's Prisma API.
Wasp invalidates frontend Query caches by looking at the Entities used by each Action/Query. Read more about Wasp's smart cache invalidation here.
- JavaScript
- TypeScript
// The 'args' object is the payload sent by the caller (most often from the client)
export const createTask = async (args, context) => {
const newTask = await context.entities.Task.create({
data: {
description: args.description,
isDone: false,
},
})
return newTask
}
export const markTaskAsDone = async (args, context) => {
await context.entities.Task.update({
where: { id: args.id },
data: { isDone: true },
})
}
import { type CreateTask, type MarkTaskAsDone } from "wasp/server/operations"
import { type Task } from "wasp/entities"
// The 'args' object is the payload sent by the caller (most often from the client)
export const createTask: CreateTask<Pick<Task, "description">, Task> = async (
args,
context
) => {
const newTask = await context.entities.Task.create({
data: {
description: args.description,
isDone: false,
},
})
return newTask
}
export const markTaskAsDone: MarkTaskAsDone<Pick<Task, "id">, void> = async (
args,
context
) => {
await context.entities.Task.update({
where: { id: args.id },
data: { isDone: true },
})
}
Again, annotating the Actions is optional, but greatly improves full-stack type safety.
The object context.entities.Task exposes prisma.task from Prisma's CRUD API.
Cache Invalidation
One of the trickiest parts of managing a web app's state is making sure the data returned by the Queries is up to date. Since Wasp uses react-query for Query management, we must make sure to invalidate Queries (more specifically, their cached results managed by react-query) whenever they become stale.
It's possible to invalidate the caches manually through several mechanisms react-query provides (e.g., refetch, direct invalidation). However, since manual cache invalidation quickly becomes complex and error-prone, Wasp offers a faster and a more effective solution to get you started: automatic Entity-based Query cache invalidation. Because Actions can (and most often do) modify the state while Queries read it, Wasp invalidates a Query's cache whenever an Action that uses the same Entity is executed.
For example, if the Action createTask and Query getTasks both use the Entity Task, executing createTask may cause the cached result of getTasks to become outdated. In response, Wasp will invalidate it, causing getTasks to refetch data from the server and update it.
In practice, this means that Wasp keeps the Queries "fresh" without requiring you to think about cache invalidation.
On the other hand, this kind of automatic cache invalidation can become wasteful (some updates might not be necessary) and will only work for Entities. If that's an issue, you can use the mechanisms provided by react-query for now, and expect more direct support in Wasp for handling those use cases in a nice, elegant way.
If you wish to optimistically set cache values after performing an Action, you can do so using optimistic updates. Configure them using Wasp's useAction hook. This is currently the only manual cache invalidation mechanism Wasp supports natively. For everything else, you can always rely on react-query.
Differences Between Queries and Actions
Actions and Queries are two closely related concepts in Wasp. They might seem to perform similar tasks, but Wasp treats them differently, and each concept represents a different thing.
Here are the key differences between Queries and Actions:
- Actions can (and often should) modify the server's state, while Queries are only permitted to read it. Wasp relies on you adhering to this convention when performing cache invalidations, so it's crucial to follow it.
- Actions don't need to be reactive, so you can call them directly. However, Wasp does provide a
useActionReact hook for adding extra behavior to the Action (like optimistic updates). actionspecs in Wasp are mostly identical toqueryspecs. The only difference lies in the spec's name.
API Reference
Specifying Actions in Wasp Spec
action »
All the options for declaring an action in the Wasp Spec.
wasp/client/operations »
The functions Wasp generates for calling your Actions on the client.
wasp/server/operations »
The functions Wasp generates for calling your Actions on the server, and the types for implementing them.
Implementing Actions
AuthenticatedActionDefinition »
The arguments and return value of an Action's implementation, and the generic type Wasp generates for typing it.
UnauthenticatedActionDefinition »
The same, for Actions that don't use auth.
HttpError »
The error you throw to send an HTTP error response to the client.
The useAction Hook and Optimistic Updates
useAction »
All the arguments of the useAction hook, and how to use react-query's lower-level API directly.
OptimisticUpdateDefinition »
How to define an optimistic update, and the rules it must follow.