Skip to content

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.

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 needed
export 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 });
},
};

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/* and pr{number}--{project}.tma.sh/api/*

Custom domains currently proxy static assets only, not same-origin /api/*.

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);
},
};

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/client with a database URL and auth token stored as environment variables
  • PlanetScale — use @planetscale/database with 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;

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;
ResourceLimit
API route bundle10 MB
Bot handler bundle (bot/index.ts or .js)10 MB
Static output total800 MB
Code-like static file (.html, .css, .js, .mjs, .cjs, .wasm, .map, .webmanifest)10 MB per file
Other static file25 MB per file
Runtime request limitsInherited from Cloudflare Workers platform

Static output limits are validated during deployment before files are uploaded to R2.

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.