API Routes
TMA.sh lets you add server-side logic to your Mini App by creating a server/api/index.ts file that exports a standard Fetch API handler. At build time, TMA.sh detects this file, bundles it with esbuild, and deploys it to Cloudflare Workers for Platforms.
The contract
Section titled “The contract”Your entry file must export a default object with a fetch method that receives a standard Request and returns a Response. This is the standard Cloudflare Workers interface — any framework (or no framework) that produces this shape works:
// server/api/index.ts -- plain fetch handler, no framework neededexport default { fetch(request: Request, env: Record<string, unknown>) { const url = new URL(request.url);
if (url.pathname === '/api/health') { return Response.json({ status: 'ok' }); }
return Response.json({ error: 'Not found' }, { status: 404 }); },};Using Hono (recommended)
Section titled “Using Hono (recommended)”Hono is the recommended framework for API routes because the TMA SDK’s requireUser() auth middleware is built as a Hono middleware. But it is not required — you can use itty-router, worktop, or write plain fetch handlers.
import { Hono } from 'hono';
const app = new Hono();
app.get('/api/health', (c) => c.json({ status: 'ok' }));
app.post('/api/items', async (c) => { const body = await c.req.json(); return c.json({ created: true, item: body });});
export default app;Deploy as usual with tma deploy. TMA.sh will detect the API entry point and bundle it automatically alongside your static assets.
Routing options for deployed API routes:
- Dedicated host:
{project}--api.tma.sh/* - Dedicated preview host:
pr{number}--{project}--api.tma.sh/* - Same-origin proxy on app hosts:
{project}.tma.sh/api/*andpr{number}--{project}.tma.sh/api/*
Custom domains currently proxy static assets only, not same-origin /api/*.
Available bindings
Section titled “Available bindings”API routes run on Cloudflare Workers and have access to the following bindings via the env parameter:
- KV — a per-project KV namespace, provisioned automatically
- DB — a per-project D1 (SQLite) database, available when managed DB provisioning/migrations are applied for the project (Pro and Team plans only)
- Environment variables — secrets and configuration set via the dashboard or
tma env
With Hono, use TypeScript generics to get typed access:
import { Hono } from 'hono';
type Env = { Bindings: { KV: KVNamespace; MY_SECRET: string; DATABASE_URL: string; };};
const app = new Hono<Env>();
app.get('/api/config', async (c) => { const cached = await c.env.KV.get('app-config'); if (cached) { return c.json(JSON.parse(cached)); }
const config = { version: '1.0.0' }; await c.env.KV.put('app-config', JSON.stringify(config), { expirationTtl: 3600, });
return c.json(config);});
export default app;With a plain fetch handler, access bindings directly from env:
export default { async fetch(request: Request, env: { KV: KVNamespace; MY_SECRET: string }) { const cached = await env.KV.get('app-config'); if (cached) { return Response.json(JSON.parse(cached)); }
const config = { version: '1.0.0' }; await env.KV.put('app-config', JSON.stringify(config), { expirationTtl: 3600, });
return Response.json(config); },};External databases
Section titled “External databases”For use cases that require features beyond what the managed D1 database provides, connect to an external database from your API routes:
- Supabase — use the Supabase client with your project’s JWT (see Authentication)
- Turso — use
@libsql/clientwith a database URL and auth token stored as environment variables - PlanetScale — use
@planetscale/databasewith a connection string
For relational data within the platform, see the Managed Database guide to set up a per-project D1 database.
import { Hono } from 'hono';import { createClient } from '@libsql/client';
type Env = { Bindings: { TURSO_URL: string; TURSO_AUTH_TOKEN: string; };};
const app = new Hono<Env>();
app.get('/api/users', async (c) => { const db = createClient({ url: c.env.TURSO_URL, authToken: c.env.TURSO_AUTH_TOKEN, });
const result = await db.execute('SELECT * FROM users LIMIT 50'); return c.json(result.rows);});
export default app;Middleware
Section titled “Middleware”If you use Hono, its middleware works as expected. Common patterns include CORS, logging, and authentication:
import { Hono } from 'hono';import { cors } from 'hono/cors';
const app = new Hono();
app.use('/api/*', cors({ origin: ['https://myapp.tma.sh'], allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],}));
app.get('/api/data', (c) => c.json({ hello: 'world' }));
export default app;Limits
Section titled “Limits”| Resource | Limit |
|---|---|
| API route bundle | 10 MB |
Bot handler bundle (bot/index.ts or .js) | 10 MB |
| Static output total | 800 MB |
Code-like static file (.html, .css, .js, .mjs, .cjs, .wasm, .map, .webmanifest) | 10 MB per file |
| Other static file | 25 MB per file |
| Runtime request limits | Inherited from Cloudflare Workers platform |
Static output limits are validated during deployment before files are uploaded to R2.
Route organization
Section titled “Route organization”For larger APIs with Hono, use route grouping:
import { Hono } from 'hono';import { users } from './routes/users';import { items } from './routes/items';
const app = new Hono();
app.route('/api/users', users);app.route('/api/items', items);
export default app;All routes imported from subdirectories will be included in the esbuild bundle automatically. Only server/api/index.ts is used as the entry point.