Documentation
Query
API functions

API functions

API functions are user-defined TypeScript functions that handle web requests. You can use them to customize the API layer of your app with complex SQL queries, authentication, data from external sources, and more.

API functions are built on top of Hono, a fast and lightweight routing framework.

Example projects

These example apps demonstrate how to use API functions.

  • Basic - An ERC20 app that responds to GET requests and uses the Select API to build custom SQL queries.
  • tRPC - An app that creates a tRPC server and a script that uses a tRPC client with end-to-end type safety.

Get started

Upgrade to >=0.5.0

API functions are available starting from version 0.5.0. Read the migration guide for more details.

Create src/api/index.ts file

To enable API functions, create a file named src/api/index.ts with the following code. You can register API functions in any .ts file in the src/api/ directory.

src/api/index.ts
import { ponder } from "@/generated";
 
ponder.get("/hello", (c) => {
  return c.text("Hello, world!");
});

Send a request

Visit http://localhost:42069/hello in your browser to see the response.

Response
Hello, world!

Register GraphQL middleware

⚠️

Once you create an API function file, you have "opted in" to API functions and your app will not serve the standard GraphQL API by default.

To continue using the standard GraphQL API, register the graphql middleware exported from @ponder/core.

src/api/index.ts
import { ponder } from "@/generated";
import { graphql } from "@ponder/core";
 
ponder.use("/", graphql());
ponder.use("/graphql", graphql());
 
// ...

Query the database

API functions can query the database using the read-only Select API, a type-safe query builder powered by Drizzle. The Select API supports complex filters, joins, aggregations, set operations, and more.

The Select API is only available within API functions. Indexing functions use the Store API (findUnique, upsert, etc) which supports writes and is reorg-aware.

Select

The API function context contains a built-in database client (db) and an object for each table in your schema (tables). These objects are type-safe – changes to your ponder.schema.ts file will be reflected immediately.

To build a query, use c.db.select() and include a table object using .from(c.tables.TableName).

ponder.schema.ts
import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Account: p.createTable({
    id: p.string(),
    balance: p.bigint(),
  }),
}));
src/api/index.ts
import { ponder } from "@/generated";
 
ponder.get("/account/:address", async (c) => {
  const address = c.req.param("address");
 
  const account = await c.db.select().from(c.tables.Account).limit(1);
 
  return c.json(account);
});

To build more complex queries, use join, groupBy, where, orderBy, limit, and other methods. Drizzle's filter & conditional operators (like eq, gte, and or) are re-exported by @ponder/core.

For more details, please reference the Drizzle documentation.

src/api/index.ts
import { ponder } from "@/generated";
import { gte } from "@ponder/core";
 
ponder.get("/whales", async (c) => {
  const { Account } = c.tables;
 
  const whales = await c.db
    .select({ address: Account.id, balance: Account.balance })
    .from(Account.id)
    .where(gte(TransferEvent.balance, 1_000_000_000n))
    .limit(1);
 
  return c.json(whales);
});

Execute

To run raw SQL queries, use db.execute(...) with the sql utility function. Read more about the sql function.

src/api/index.ts
import { ponder } from "@/generated";
import { sql } from "@ponder/core";
 
ponder.get("/:token/ticker", async (c) => {
  const token = c.req.param("token");
 
  const result = await c.db.execute(
    sql`SELECT ticker FROM "Token" WHERE id = ${token}`
  );
  const ticker = result.rows[0]?.ticker;
 
  return c.text(ticker);
});

API reference

get()

Use ponder.get() to handle HTTP GET requests. The c context object contains the request, response helpers, and the database connection.

src/api/index.ts
import { ponder } from "@/generated";
 
ponder.get("/account/:address", async (c) => {
  const { Account } = c.tables;
  const address = c.req.param("address");
 
  const account = await c.db
    .select()
    .from(Account)
    .where({ id: address })
    .first();
 
  if (account) {
    return c.json(account);
  } else {
    return c.status(404).json({ error: "Account not found" });
  }
});

post()

API functions cannot write to the database, even when handling POST requests.

Use ponder.post() to handle HTTP POST requests.

In this example, we calculate the volume of transfers for each recipient within a given time range. The fromTimestamp and toTimestamp parameters are passed in the request body.

src/api/index.ts
import { ponder } from "@/generated";
import { and, gte, sum } from "@ponder/core";
 
ponder.post("/volume", async (c) => {
  const { TransferEvent } = c.tables;
 
  const body = await c.req.json();
  const { fromTimestamp, toTimestamp } = body;
 
  const volumeChartData = await c.db
    .select({
      to: TransferEvent.toId,
      volume: sum(TransferEvent.amount),
    })
    .from(TransferEvent)
    .groupBy(TransferEvent.toId)
    .where(
      and(
        gte(TransferEvent.timestamp, fromTimestamp),
        lte(TransferEvent.timestamp, toTimestamp)
      )
    )
    .limit(1);
 
  return c.json(volumeChartData);
});

use()

Use ponder.use(...) to add middleware to your API functions. Middleware functions can modify the request and response objects, add logs, authenticate requests, and more. Read more about Hono middleware.

src/api/index.ts
import { ponder } from "@/generated";
 
ponder.use((c, next) => {
  console.log("Request received:", c.req.url);
  return next();
});

hono

Use ponder.hono to access the underlying Hono instance.

src/api/index.ts
import { ponder } from "@/generated";
 
ponder.hono.notFound((c) => {
  return c.text("Custom 404 Message", 404);
});
 
// ...

Reserved routes

If you register API functions that conflict with these internal routes, the build will fail.

  • /health: Returns a 200 status code immediately after the app starts running. Read more about healthchecks.
  • /ready: Returns a 200 status code after the app has completed the historical backfill and is available to serve traffic. Read more about heatlthchecks.
  • /metrics: Returns Prometheus metrics. Read more about metrics.
  • /status: Returns indexing status object. Read more about indexing status.