Skip to content

API Routes

TMA.sh lets you add server-side logic to your Mini App by creating a Hono app in server/api/index.ts. At build time, TMA.sh detects this file, bundles it with esbuild, and deploys it to Cloudflare Workers for Platforms. Your API becomes available at {project}--api.tma.sh/api/*.

Create a server/api/index.ts file in the root of your project:

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.

Your static SPA is served at {project}.tma.sh and your API at {project}--api.tma.sh/api/*. Same-origin /api/* requests on {project}.tma.sh are also proxied to the API worker, so you can use relative paths like /api/health from your client-side code.

API routes run on Cloudflare Workers and have access to the following bindings:

  • KV — a per-project KV namespace, provisioned automatically
  • Environment variables — secrets and configuration set via the dashboard or tma env

Use TypeScript generics to get typed access to bindings:

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;

Per-project D1 is not available in the current release. For relational data, 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
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;

Hono’s 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
Bundle size10 MB
Request body100 MB
Execution time30 seconds
Subrequests50 per request

For larger APIs, use Hono’s 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.