Dev API

During emberkit dev, EmberKit can intercept /api and /api/* and run your backend logic in Node — without a separate proxy server.

When to use it

ApproachBest for
File-based src/routes/_api/*REST handlers colocated with routes, similar to Next.js Route Handlers
Custom handler moduleExisting Express-style routers, Turso migrations on first request, shared handleApiRequest for dev and Workers
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 devApi from this file and registers dev middleware
  • Enables sqlRawPlugin for *.sql?raw imports
  • Falls back to src/routes/_api/* when devApi is 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.

How requests are routed

  1. Request URL pathname is /api or starts with /api/.
  2. Dev middleware runs before SSR page rendering (SSR skips API URLs).
  3. 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:

ExportDescription
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