Actions and API 

Your application comes bundled with its own auto-generated API, which can be called from both your frontend and backend. This API includes a GraphQL endpoint as well as type-safe JavaScript and React clients, making it easy to interact with your backend in different contexts.

Calling the API

  • In the frontend: The API can be called directly using the JavaScript/React client.
  • In the backend: The API is accessible within server-side functions, including Gadget actions.

This automatically generated API includes documentation exclusive to your Gadget application. These pages provide a more detailed overview of how to use your automatically generated API.

Using the API in Backend Actions

Gadget actions accept an api as an argument for your run function. This api object is an instance of the generated JavaScript client for your Gadget application. It works the same way in an action as it does elsewhere and has all the same functions documented in the API Reference. You can use the api object to fetch other data in an action with findOne, findMany, findById, and other find actions:

JavaScript
// example: send a notification to the user who owns the record if they've opted in import twilio from "../../twilio-client"; export const run: ActionRun = async ({ api, record }) => { const creator = await api.user.findOne(record.creatorId, { select: { phoneNumber: true, wantsNotifications: true }, // Selecting only necessary fields for performance }); if (creator?.wantsNotifications) { await twilio.sendSMS({ to: creator.phoneNumber, from: "+11112223333", body: `Notification: ${record.title} was updated`, }); } };
// example: send a notification to the user who owns the record if they've opted in import twilio from "../../twilio-client"; export const run: ActionRun = async ({ api, record }) => { const creator = await api.user.findOne(record.creatorId, { select: { phoneNumber: true, wantsNotifications: true }, // Selecting only necessary fields for performance }); if (creator?.wantsNotifications) { await twilio.sendSMS({ to: creator.phoneNumber, from: "+11112223333", body: `Notification: ${record.title} was updated`, }); } };

Or, you can use the api object to change other data in your application through create, update, and delete actions:

JavaScript
// example: save an audit log record when this record changes export const run: ActionRun = async ({ api, record }) => { await api.auditLog.create({ data: { action: "UPDATE", recordId: record.id, changes: record.changes, }, }); };
// example: save an audit log record when this record changes export const run: ActionRun = async ({ api, record }) => { await api.auditLog.create({ data: { action: "UPDATE", recordId: record.id, changes: record.changes, }, }); };

Public API vs Internal API 

Your application has two different levels of API: the Public API and the Internal API. The Public API runs the high-level actions you've defined in your app, and the Internal API runs low-level operations that change the database.

Depending on what you're trying to accomplish with your action, it is sometimes appropriate to use the Public API of your application via api.someModel.create, and sometimes it's appropriate to use the Internal API via api.internal.someModel.create.

Generally, the Public API should be preferred. The Public API for a Gadget application performs all the steps of an action, including checking permissions, performing data validation, and executing the run and onSuccess function. The Public API can be accessed in both the backend and the frontend of your application.

For example, you may choose to use the Public API to perform a CRUD action on a small batch of data.

Using the public API to create a record
JavaScript
await api.blogPost.create({ title: "My First Post", content: "This is my first post", date: new Date(), });
await api.blogPost.create({ title: "My First Post", content: "This is my first post", date: new Date(), });

Sometimes, though, you may want to skip all the steps of the Public API, and just interact with the database. The Internal API does just this. Unlike the Public API, the Internal API only performs database interactions, and skips any permission-checking, most validations (except required and unique), or custom code in your run and onSuccess functions. The Internal API can be used by running api.internal.someModel.<action name>. You can think of the Internal API like raw SQL statements in other frameworks where you might run explicit INSERT or UPDATE statements that skip over the other bits of the framework.

The Internal API is significantly faster and is often used for building high-volume scripts that need to import data or touch many records quickly.

Using the Internal API to create many records
JavaScript
await api.internal.blogPost.bulkCreate([ { title: "post1", content: "Hello world", date: "2024-04-01" }, ]);
await api.internal.blogPost.bulkCreate([ { title: "post1", content: "Hello world", date: "2024-04-01" }, ]);

However, because the Internal API skips important logic for actions, it is generally not recommended for use unless you have an explicit reason. Additionally, the Internal API can only be accessed in the backend of your application, not in the frontend.

actAsSession and actAsAdmin 

Roles play an important part in actions in Gadget. A user (and their session) have a specific role, which limits the interactions they can have with the database.

When calling an action from the frontend, Gadget will first ensure that the user has permission to perform this action. If the user has access, they will be allowed to perform whatever code is inside of the action.

By default, an api inside of an action will always have system admin privileges. This means inside your actions, the api object can use any api endpoints without any restrictions. This is very helpful for implementing the internal logic of your app.

However, there are times where you may want the api object to act with the same permissions as the session making the request. In this case you can use api.actAsSession.

As an example, pretend you are making a blog post website. You want to have exclusive blog posts for signed-in users. If an unauthenticated user accesses the website, you wish to show different posts. In this case, using api.actAsSession would be useful:

api/actions/getPublishedPosts.js
JavaScript
export const run: ActionRun = async ({ api }) => { // assume that this action is called from an unauthenticated session // by default api has admin privileges, so this will be all posts const allPosts = await api.post.findMany(); // if we only want published posts, using api we need to filter for them const publishedPosts = await api.post.findMany({ filter: { published: { equals: true, }, }, }); // assume that there is a filter on the unauthenticated role's read access to post // in this case publishedPosts will be the same as unauthenticatedAccessiblePosts const unauthenticatedAccessiblePosts = await api.actAsSession.post.findMany(); };
export const run: ActionRun = async ({ api }) => { // assume that this action is called from an unauthenticated session // by default api has admin privileges, so this will be all posts const allPosts = await api.post.findMany(); // if we only want published posts, using api we need to filter for them const publishedPosts = await api.post.findMany({ filter: { published: { equals: true, }, }, }); // assume that there is a filter on the unauthenticated role's read access to post // in this case publishedPosts will be the same as unauthenticatedAccessiblePosts const unauthenticatedAccessiblePosts = await api.actAsSession.post.findMany(); };

api.actsAsSession is only available when your action or route has been triggered by a call from a client that is using session authentication. Gadget uses session authentication by default for clients created in the browser, but for clients authenticating with an API Key, .actsAsSession will throw an error. You can use the action context to know if there is a session available:

JavaScript
export const run: ActionRun = async ({ api, session }) => { if (session) { const widgets = await api.actAsSession.widget.findMany(); } };
export const run: ActionRun = async ({ api, session }) => { if (session) { const widgets = await api.actAsSession.widget.findMany(); } };

Infinite loops 

Invoking actions within actions can cause infinite loops, so care must be taken to avoid this. If you invoke the currently underway action from within itself, your run function will run again, potentially invoking the action again in a chain. Or, if you invoke an action in model B from model A, but model B invokes an action in model A, you can create a chain of action invocations that never ends. Action chains like this can show up in the logs as timeouts or errors that happen at an arbitrary point in an action.

To avoid infinite loops, you must break the loop somehow. There are a couple of options for this:

  • Use record.changed('someField') to only run a nested action some of the time when data you care about has changed
  • Use the Internal API instead of the Public API (api.internal.someModel.update instead of api.someModel.update) to make changes, skipping the action that re-triggers the loop.

Calling third-party APIs from your actions is common, but it's important to be aware that third-party APIs may trigger subsequent actions in your Gadget app.

For example, if you have a Gadget app connected to a Shopify store with a connected shopifyProduct model, every time Shopify sends the app a product/updated webhook, Gadget will run the update action. If the business logic inside the update action makes API calls back to Shopify to update the product again (say to update the tags of a product in response to a change in the other fields), the API call back to Shopify will trigger a second webhook sent back to Gadget. Without care, this can cause an infinite loop of actions triggering webhooks that retrigger the actions.

See Action infinite loops for strategies to break this infinite loop.

Was this page helpful?