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 Drizzle 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.
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.
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
.
import { ponder } from "@/generated";
import { graphql } from "@ponder/core";
ponder.use("/", graphql());
ponder.use("/graphql", graphql());
// ...
Query the database
The API function context includes a ready-to-use Drizzle database client at c.db
. To query the database, import table objects from ponder.schema.ts
and pass them to c.db.select()
or use relational queries.
Select
Here's a simple query using the Drizzle select
query builder.
import { ponder } from "@/generated";
import { accounts } from "../../ponder.schema";
ponder.get("/account/:address", async (c) => {
const address = c.req.param("address");
const account = await c.db
.select()
.from(accounts)
.where(eq(accounts.address, address))
.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
. Visit the Drizzle documentation for more details.
Relational queries
Drizzle's relational query builder (AKA Drizzle Queries) offers a great developer experience for complex queries. The c.db.query
object exposes the raw Drizzle relational query builder.
Here's an example that uses the relational query builder in an API function to find the 10 largest trades in the past hour joined with the account that made the trade. Visit the Drizzle Queries documentation for more details.
import { accounts, tradeEvents } from "../ponder.schema";
import { eq, and, gte, inArray, sql } from "drizzle-orm";
ponder.get("/hot-trades", async (c) => {
const trades = await c.db.query.tradeEvents.findMany({
where: (table, { gt, gte, and }) =>
and(
gt(table.amount, 1_000n),
gte(table.timestamp, Date.now() - 1000 * 60 * 60)
),
limit: 10,
with: { account: true },
});
return c.json(trades);
});
API reference
get()
Use ponder.get()
to handle HTTP GET
requests. The c
context object contains the request, response helpers, and the database connection.
import { ponder } from "@/generated";
import { eq } from "@ponder/core";
import { accounts } from "../../ponder.schema";
ponder.get("/account/:address", async (c) => {
const address = c.req.param("address");
const account = await c.db
.select()
.from(accounts)
.where(eq(accounts.address, 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.
import { ponder } from "@/generated";
import { and, gte, sum } from "@ponder/core";
import { transferEvents } from "../../ponder.schema";
ponder.post("/volume", async (c) => {
const body = await c.req.json();
const { fromTimestamp, toTimestamp } = body;
const volumeChartData = await c.db
.select({
to: transferEvents.toId,
volume: sum(transferEvents.amount),
})
.from(transferEvents)
.groupBy(transferEvents.toId)
.where(
and(
gte(transferEvents.timestamp, fromTimestamp),
lte(transferEvents.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.
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.
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 a200
status code immediately after the app starts running. Read more about healthchecks./ready
: Returns a200
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.