API access to data 

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 (e.g. a query that is powered by the user's input on the frontend) or simple, predefined queries. For handling data in aggregates, or complex predefined queries, we recommend using computed fields.

Each time you add a new model to your project, Gadget instantly generates a public and private API to interact with the records stored in that table:

  • The public API is how your production application runs its business logic. The public API is described in actions that run both the database work we want our backend to do, along with any additional logic you specify. For example, a blog might have a createBlog action that persists blog data to the Gadget database before running a function to send a success email to the author.

  • The internal API is how you quickly interact with data (create, read, update, delete) at the database level, without running any additional business logic.

Your production application's frontend will typically make API calls to your Gadget backend via your public API, whereas your project's internal API is primarily used to fetch information to run the server side functions that power your actions.

Learn more about when to use the public API vs the internal API.

Data aggregation using the API 

While, computed fields are typically the recommended approach to handling predefined aggregate queries (e.g. counts and sums) in Gadget, they cannot take in dynamic filters. In the event that your app has a query that requires dynamic filtering, you will need to aggregate the information you need at read-time by paginating, and produce your own filtered sums and counts.


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.

The page size is controlled using the first or last GraphQL field arguments.


The downside to paginating through all records is performance. If the total count is 100000 records, walking through them in increments will take a long time. Especially considering that the maximum page size is 250 records and the default page size of 50 records.

When to paginate 

  • You are confident that you won't have a large number of total records in your collection
  • You have dynamic filtering options that don't allow for counts to be pre-calculated

When to avoid pagination 

  • You may need to count a large number of records
  • You don't need dynamic options for filtering your counts

For instance, if you want to get the count of all products in a Shopify store:

1export async function run({ api, record, params, logger }) {
2 const shopId = "my shop id";
3 const productCount = 0;
5 // api is an instance of my Gadget API client
6 let products = await api.shopifyProduct.findMany({
7 // use the maximum page size
8 first: 250,
9 // select the minimum amount of data from the model as needed
10 select: {
11 id: true,
12 },
13 // filter by the shop id
14 // adding more filter conditions is usually a good idea, let the database handle filtering for you!
15 filter: {
16 shop: {
17 equals: shopId,
18 },
19 },
20 });
22 productCount += products.length;
24 // paginate through all our products, 250 at a time
25 while (products.hasNextPage) {
26 products = await products.nextPage();
27 productCount += products.length;
28 }
30 logger.info(
31 { productCount },
32 "contains the total number of products for a given store"
33 );

Paginating nested relationships 

When you need to paginate through nested data relationships, efficiently utilize cursors to navigate from one model of related records to the next, ensuring seamless and performance-optimized data retrieval.

For instance, let's take a look at the example below: when fetching a class record with paginated student data, then sequentially retrieving further student records, enabling efficient navigation through large sets of nested data.

1const classRecord = await api.class.findFirst({
2 select: {
3 id: true,
4 students: {
5 pageInfo: {
6 hasNextPage: true,
7 endCursor: true,
8 },
9 edges: {
10 node: {
11 id: true,
12 },
13 },
14 },
15 },
18// Using the cursor to define the end of retrieval of records within the class model so we can go and fetch from the student model
19const nextPageOfStudents = await api.student.findMany({
20 first: 50,
21 after: classRecord.students.pageInfo.endCursor,
22 select: { id: true },
25// We now define that after the end of the retrieval we go right back to the next 50 results within the class model
26const nextNextPageOfStudents = await api.student.findMany({
27 first: 50,
28 after: nextPageOfStudents.endCursor,
29 select: { id: true },

Pre-aggregating data 

Within Gadget it is highly recommended to use computed fields in most instances, however, there are a few cases where you can consider pre-aggregating data at write time.

  1. Large datasets: Pre-aggregation is particularly beneficial when dealing with large datasets where performing aggregations or complex calculations on the fly can be computationally expensive.

  2. Grouping data: As Gadget doesn't currently support computed views (yet), there are cases where you might need to group data. In these instances, using pre-aggregation strategies with a combination of computed fields will be necessary.