Gadget automatically generates and documents API endpoints that allow you to interact with your data. The API is how you fetch information for arbitrary queries, for example, a query that is powered by the user's input on the frontend, or simple, predefined queries.
Public API: The API defined by your Gadget actions. Used by your application to execute business logic and interact with the data from your frontend or backend.
Internal API: The internal API allows you to interact with data (create, read, update, delete) at the database level, without running any additional business logic. Used for low-level data manipulation and performance optimization.
Calling the public API invokes model-scoped and globally-scoped actions, which means any logic contained in an action file, including custom code, data tenancy checks, and validations, is run.
calling the public API
JavaScript
// code in customer/actions/create is run
await api.customer.create({
name: "Carl Weathers",
});
// code in customer/actions/create is run
await api.customer.create({
name: "Carl Weathers",
});
The internal API allows you to skip business logic execution, and just persist data to the database. 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:
calling the internal API
JavaScript
// no code in customer/actions/create is run
await api.internal.customer.create({
name: "Carl Weathers",
});
// no code in customer/actions/create is run
await api.internal.customer.create({
name: "Carl Weathers",
});
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, such as data migrations.
Be careful when working with the internal API. It is a powerful tool that can easily break data and violate important promises that are
enforced when using the public API.
More information about your app's internal API can be found in the internal API docs.
Calling your API
There are different ways to call your API, depending on whether you are calling it from the frontend or backend, and whether you are using a framework like React Router v7.
Autocomponents are pre-built React components used to power forms and tables.
render a form for the widget model
React
import { api } from "../api";
export default function Example() {
// the widget.create action is called on form submission
return <AutoForm action={api.widget.create} />;
}
import { api } from "../api";
export default function Example() {
// the widget.create action is called on form submission
return <AutoForm action={api.widget.create} />;
}
They use your app's API under the hood to read and write data without the need to call the API explicitly.
Calling your API from Remix/React Router loader and action functions
In Remix and React Router, you can call your API from loader and action functions. These functions run on the server and can be used to fetch data before rendering a page, and handle actions on the server.
Your api is available as part of the function context:
a Remix/React Router v7 loader function
JavaScript
// read blog data on the server
export const loader = async ({ context, params }: Route.LoaderArgs) => {
const blog = context.api.blog.findById(params.id);
return {
blog,
};
};
// read blog data on the server
export const loader = async ({ context, params }: Route.LoaderArgs) => {
const blog = context.api.blog.findById(params.id);
return {
blog,
};
};
An api client is provided as context to all Gadget actions and routes and should be how you make API calls in your backend. api includes all your app's actions in addition to functions for reading data. These are described in your Gadget app's API reference, with each model in the Schema section having their own reference to the functions.
Actions are unique to your app. Provided read APIs are described below:
findOne()
findOne fetches the first record from your Gadget database that matches the given id. If no record is found with a matching id, an error will be thrown.
findBy is similar to findOne, except that this function is not limited to id for searching.
Whenever you add a uniqueness validation to a field in your model, a findBy() function is created specifically for that model and field. Because the default id field on your models is unique by default, there will always be a findById() function.
If you also add a uniqueness validation to another field, say the title field on a blogPost model, Gadget will create a findByTitle() function: api.blogPost.findByTitle().
If no record is found with a matching id, an error will be thrown.
findMany fetches one page of records from a given model. An empty array will be returned if no records are found.
A filter condition can be provided to narrow down the records that will be returned.
Returned pages of findMany results can also be paginated.
using findMany
JavaScript
export const run: ActionRun = async ({ params, logger, api, connections }) => {
const customer = api.customer.findMany();
//logs the length of the page
logger.info(customer.length);
};
export const run: ActionRun = async ({ params, logger, api, connections }) => {
const customer = api.customer.findMany();
//logs the length of the page
logger.info(customer.length);
};
maybeFindX()
You can use the maybeFind functions to return null when no record is found instead of throwing an error.
For example, maybeFindOne(1) returns null if no customer is found where id === 1.
This means findOne has an equivalent maybeFindOne, findByX has an equivalent maybeFindByX, and findFirst has an equivalent maybeFindFirst.
These maybe functions act the same as their counterpart, except they return null if no matching record is found.
using maybeFindOne
JavaScript
export const run: ActionRun = async ({ params, logger, api, connections }) => {
const customer = api.customer.findMany();
//logs the length of the page
logger.info(customer.length);
};
export const run: ActionRun = async ({ params, logger, api, connections }) => {
const customer = api.customer.findMany();
//logs the length of the page
logger.info(customer.length);
};
Data aggregation using the API
Gadget supports ways to aggregate data, including:
Computed fields, which return one pre-aggregated value per record of a model.
Computed viewsbeta, which compute aggregate values across many records and/or models.
Client side pagination, which allows you to paginate through a large number of records to compute an aggregate in JavaScript.
Computed views are the recommended approach to handling predefined aggregate queries (like counts and sums) in Gadget. They can take in dynamic variables for controlling filters, sorts, aggregations, and pagination, and are executed server-side within the database for the best performance.
If you want to run more complicated business logic in JavaScript/TypeScript, or process streams of data, you can also use client-side aggregation by reading pages of data from the API and producing your own aggregate values. Client side aggregation is much slower than computed views or computed fields, so use it as a last resort.
Client-side aggregation with pagination
To produce read-time aggregates with dynamic filtering, you can fetch the desired records and paginate through them to get the desired aggregate data.
This allows you to apply any filters you want to the GraphQL query and ensures you get an accurate count.
You can use findMany to fetch pages of records. By default, a single findMany request will return 100 records. first or last parameters can be used to control the number of records returned per page, with a maximum of 250 records per page.
To paginate, or grab another page of records, you can use the nextPage() or previousPage() functions on the returned result of the findMany call. A while loop can be used to keep fetching pages until there are no more pages left:
fetch records and paginate
JavaScript
// get 250 records
let records = await api.webflow.site.findMany({
first: 250,
});
// do something with the records
// fetch the next 250 records
while (records.hasNextPage) {
records = await records.nextPage();
// do something with the records
}
// get 250 records
let records = await api.webflow.site.findMany({
first: 250,
});
// do something with the records
// fetch the next 250 records
while (records.hasNextPage) {
records = await records.nextPage();
// do something with the records
}
See your app's API docs for more information and examples of pagination.
When to paginate
You are displaying pages on content on the frontend and users can navigate through them.
You need to pre-computed aggregates that cannot be calculated using computed views or computed fields.
When to avoid pagination
You need to paginate through all records on a model, or all related records.
Most of the time: you should almost always try using computed fields or computed views instead of paginating though records. Computed views and fields are both cached, so repeated reads are faster using these data access methods.
Limitations
Paginating through all records client side performs poorly. The maximum page size for a paginated read in Gadget is 250 records, so if you need to aggregate 100000 records, walking through them will require fetching 400 pages of data.
Paginating nested relationships
It is also possible to paginate through nested data relationships using cursors for performance-optimized data retrieval.
The maximum page size for nested relationships is 100 records.
You must explicitly select the pageInfo and subsequent cursor fields (hasNextPage and hasPreviousPage, endCursor, and startCursor) in your query to enable pagination on nested relationships.
For example, you can fetching a school record with paginated student data, then fetch additional pages of student records:
paginate through related model records
JavaScript
// fetch students enrolled at a school
const schoolRecord = await api.school.findFirst({
select: {
id: true,
students: {
// pageInfo provides cursors for pagination
pageInfo: {
hasNextPage: true,
endCursor: true,
},
edges: {
node: {
id: true,
},
},
},
},
});
// Using the cursor to define the end of retrieval of records within the school model so we can go and fetch from the student model
const nextPageOfStudents = await api.student.findMany({
first: 50,
after: schoolRecord.students.pageInfo.endCursor,
select: { id: true },
});
// We now define that after the end of the retrieval we go right back to the next 50 results within the school model
const nextNextPageOfStudents = await api.student.findMany({
first: 50,
after: nextPageOfStudents.endCursor,
select: { id: true },
});
// fetch students enrolled at a school
const schoolRecord = await api.school.findFirst({
select: {
id: true,
students: {
// pageInfo provides cursors for pagination
pageInfo: {
hasNextPage: true,
endCursor: true,
},
edges: {
node: {
id: true,
},
},
},
},
});
// Using the cursor to define the end of retrieval of records within the school model so we can go and fetch from the student model
const nextPageOfStudents = await api.student.findMany({
first: 50,
after: schoolRecord.students.pageInfo.endCursor,
select: { id: true },
});
// We now define that after the end of the retrieval we go right back to the next 50 results within the school model
const nextNextPageOfStudents = await api.student.findMany({
first: 50,
after: nextPageOfStudents.endCursor,
select: { id: true },
});
Pre-aggregating data
When possible, Gadget recommends using computed views or computed fields to aggregate data. However, there are a few cases where you can consider pre-aggregating data at write time:
Maximum performance is necessary: Computed views and computed fields are executed server-side within the database, so they are faster than client-side aggregation, but they re-compute the aggregate value each time they are read. If you need to compute an aggregate value that is read very frequently, you may need to pre-compute the value at write time. Serving pre-aggregated data is faster than re-computing the aggregate on each request.
Huge datasets: If the pre-aggregation is particularly beneficial when dealing with large datasets where performing aggregations or complex calculations on the fly can time out from being too computationally expensive.
To pre-aggregate data, add a new field to your model to hold your aggregate, and then update it in your action code when a record is created, updated, or deleted.