Dev API
During emberkit dev, EmberKit can intercept /api and /api/* and run your backend logic in Node — without a separate proxy server.
Production
Dev API middleware is development only. In production, wire APIs through your host (Cloudflare Workers, Node server, etc.). The Orange Ember website uses worker.ts for production and devApiPlugin locally.
When to use it
| Approach | Best for |
|---|---|
File-based src/routes/_api/* | REST handlers colocated with routes, similar to Next.js Route Handlers |
| Custom handler module | Existing Express-style routers, Turso migrations on first request, shared handleApiRequest for dev and Workers |
Recommended — emberkit.config.ts
import { defineConfig } from '@emberkit/core';
import { emberkitVitePlugin } from '@emberkit/core/vite-plugin';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
mode: 'ssr',
devApi: {
handler: './src/server/api-router.node.ts',
export: 'handleApiRequestNode',
prefix: '/api', // optional, default '/api'
},
vite: {
plugins: [emberkitVitePlugin(), tailwindcss()],
},
});
emberkitVitePlugin() automatically:
- Loads
devApifrom this file and registers dev middleware - Enables
sqlRawPluginfor*.sql?rawimports - Falls back to
src/routes/_api/*whendevApiis omitted
Option B — explicit Vite plugins (advanced)
Only needed if you cannot use devApi in emberkit.config.ts or need a custom plugin order:
import {
devApiPlugin,
emberkitVitePlugin,
sqlRawPlugin,
} from '@emberkit/core/vite-plugin';
export const appVitePlugins = [
sqlRawPlugin(),
devApiPlugin({
handler: './src/server/api-router.node.ts',
export: 'handleApiRequestNode',
}),
emberkitVitePlugin(),
];
Do not duplicate devApi in config and devApiPlugin at the same time.
Custom Node handler
Export a function with Node’s IncomingMessage / ServerResponse signature:
// src/server/api-router.node.ts
import type { IncomingMessage, ServerResponse } from 'node:http';
import { handleApiRequest } from './api-router.ts';
export async function handleApiRequestNode(
req: IncomingMessage,
res: ServerResponse,
): Promise<void> {
const host = req.headers.host ?? 'localhost';
const url = `http://${host}${req.url ?? '/'}`;
const request = new Request(url, { method: req.method, headers: req.headers });
const response = await handleApiRequest(request, env);
// copy status, headers, body to res ...
}
Share handleApiRequest(request, env) between dev (Node) and production (Workers) so behavior stays consistent.
Use createNodeDevApiHandler to avoid duplicating cookie-safe header bridging:
import { createNodeDevApiHandler } from '@emberkit/core/vite-plugin';
import { handleApiRequest } from './api-router.ts';
export const handleApiRequestNode = createNodeDevApiHandler(
handleApiRequest,
() => ({
TURSO_DATABASE_URL: process.env.TURSO_DATABASE_URL ?? '',
// ...
}),
);
File-based API routes
Add handlers under src/routes/_api/:
src/routes/_api/
health.ts → GET /api/health
blog/
list.ts → GET /api/blog/list
When devApi is not set in config and at least one _api route exists, EmberKit auto-enables file-based dev API routing.
Custom devApi overrides auto routing
If you set devApi.handler, file-based _api routes are not used unless you call them from your handler.
How requests are routed
- Request URL pathname is
/apior starts with/api/. - Dev middleware runs before SSR page rendering (SSR skips API URLs).
- Handler module is loaded once via
server.ssrLoadModule()and cached.
Testing locally
pnpm dev
curl -s "http://localhost:4321/api/health"
curl -s "http://localhost:4321/api/blog/list?lang=en"
If the port is busy, EmberKit picks the next free port — check the dev server banner.
Vite exports
From @emberkit/core/vite-plugin:
| Export | Description |
|---|---|
devApiPlugin(options) | Vite plugin for custom handler |
registerDevApiMiddleware(server, options) | Register middleware on an existing dev server |
registerFileBasedDevApiMiddleware(server) | Register _api file routing |
isApiRequest(url) | Returns true for /api and /api/* |
incomingMessageToRequest(req) | Convert Node request to Fetch Request |
writeFetchResponseToNode(res, response) | Write Fetch Response to Node |