Back to Blog
Tutorial11 min read2026-08-07

Deploying a Bun + Hono API from a Monorepo Subdirectory

Ship a Hono API running on Bun from a monorepo — configure rootDir to isolate the build, handle workspace dependencies, and avoid deploying the entire repo.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

You have a monorepo with three folders: apps/web (a Next.js frontend), apps/api (a Bun + Hono backend), and packages/shared (TypeScript types used by both). You want to deploy just the API, but the platform defaults to building from the repository root. It installs dependencies for the entire monorepo, tries to build the frontend and the backend simultaneously, and either fails or wastes build minutes compiling code you don't need.

The fix: rootDir. This field tells the build system to treat a subdirectory as the working directory. It runs bun install and bun run start from apps/api/, not from the repo root. The frontend stays unbuilt, the backend deploys, and you avoid the "monorepo bloat" problem.

The monorepo structure

my-monorepo/
├── apps/
│   ├── web/           # Next.js app
│   └── api/           # Bun + Hono API
├── packages/
│   └── shared/        # Shared TypeScript types
├── package.json       # Root workspace config
└── bun.lockb          # Bun lockfile

Root package.json:

{
  "name": "my-monorepo",
  "workspaces": ["apps/*", "packages/*"],
  "private": true
}

apps/api/package.json:

{
  "name": "api",
  "version": "1.0.0",
  "scripts": {
    "dev": "bun run --watch src/index.ts",
    "start": "bun run src/index.ts"
  },
  "dependencies": {
    "hono": "^4.0.0",
    "@my-monorepo/shared": "workspace:*"
  }
}

The API depends on the shared package via workspace:*. Bun resolves this by symlinking packages/shared into node_modules/@my-monorepo/shared during bun install.

The Hono app

apps/api/src/index.ts:

import { Hono } from 'hono';
import type { User } from '@my-monorepo/shared';

const app = new Hono();

app.get('/health', (c) => c.json({ status: 'ok' }));

app.get('/users/:id', (c) => {
  const user: User = {
    id: c.req.param('id'),
    name: 'Alice'
  };
  return c.json(user);
});

const port = parseInt(process.env.PORT || '3000', 10);
console.log(`Server running on port ${port}`);

export default {
  port,
  fetch: app.fetch
};

Bun's HTTP server runs via export default { fetch }. This is different from Node.js (where you'd call app.listen()), but it's the Bun-native pattern and avoids the 0.0.0.0 binding issue entirely — Bun binds to all interfaces by default.

Deploying with rootDir

Without rootDir, the platform runs bun install from the repo root, which installs dependencies for the frontend, backend, and shared package. The build works, but it's slow and wastes resources.

With rootDir: "apps/api", the platform:

  1. 1Clones the repo.
  2. 2Changes directory to apps/api.
  3. 3Runs bun install (which resolves workspace:* dependencies by reading the root package.json).
  4. 4Runs bun run start.

Here's the API deployment payload:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "container",
    "name": "hono-api",
    "repositoryName": "yourorg/my-monorepo",
    "branch": "main",
    "autoDeploy": true,
    "rootDir": "apps/api",
    "startCommand": "bun run start",
    "env": [
      { "name": "PORT", "value": "3000" }
    ]
  }'

The rootDir parameter is the key. It isolates the build to the apps/api/ subdirectory.

How workspace dependencies are resolved

When bun install runs inside apps/api/, Bun walks up the directory tree, finds the root package.json with workspaces: ["apps/*", "packages/*"], and resolves @my-monorepo/shared to ../../packages/shared.

The build system clones the entire repository, so packages/shared is present. Bun symlinks it into node_modules/@my-monorepo/shared, and the API imports types from it as if it were a published npm package.

This works for:

  • Bun workspaces
  • npm workspaces
  • Yarn workspaces
  • pnpm workspaces

The only difference is the lockfile format. The platform auto-detects the package manager (Bun if bun.lockb exists, pnpm if pnpm-lock.yaml exists, etc.).

Using pandastack.json for the API

For a cleaner deploy button workflow, commit pandastack.json to apps/api/:

{
  "type": "container",
  "name": "hono-api",
  "language": "nodejs",
  "rootDir": "apps/api",
  "startCommand": "bun run start",
  "healthCheckPath": "/health",
  "env": [
    { "key": "PORT", "value": "3000" },
    {
      "key": "DATABASE_URL",
      "description": "PostgreSQL connection string"
    }
  ]
}

Now a deploy button reads this config:

[![Deploy to PandaStack](https://dashboard.pandastack.io/deploy-button.svg)](https://dashboard.pandastack.io/deploy?repo=yourorg/my-monorepo&rootDir=apps/api)

The rootDir query parameter tells the platform to look for pandastack.json in apps/api/ instead of the repo root. The deploy screen pre-fills the start command and prompts for DATABASE_URL.

Deploying the frontend separately

To deploy the Next.js frontend from the same repo, create a second project with rootDir: "apps/web":

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "static",
    "name": "nextjs-web",
    "repositoryName": "yourorg/my-monorepo",
    "branch": "main",
    "autoDeploy": true,
    "rootDir": "apps/web",
    "buildCommand": "bun run build",
    "outputDir": "out"
  }'

Now you have two deployments from one repo:

  • https://hono-api-abc123.pandastack.app (the Bun + Hono backend)
  • https://nextjs-web-xyz456.pandastack.app (the Next.js frontend)

Both auto-deploy when you push to main. The build system is smart enough to detect which subdirectory changed and only redeploy the affected project (this is a planned feature — currently, both redeploy on every push).

Debugging "Workspace dependency not found"

If the build fails with "Cannot find package '@my-monorepo/shared'", check:

  1. 1The root package.json includes "workspaces": ["apps/*", "packages/*"].
  2. 2The shared package has a package.json with "name": "@my-monorepo/shared".
  3. 3The lockfile is committed (Bun workspaces need bun.lockb in the repo root).

Without a lockfile, Bun might resolve dependencies differently between local and CI builds.

Handling build scripts in the shared package

If packages/shared has a build step (e.g. compiling TypeScript), you need to run it before starting the API. Add a postinstall script to apps/api/package.json:

{
  "scripts": {
    "postinstall": "cd ../../packages/shared && bun run build",
    "start": "bun run src/index.ts"
  }
}

This runs automatically after bun install, ensuring the shared package is built before the API starts.

Alternatively, use a compiled dist/ directory in the shared package and import from there:

{
  "name": "@my-monorepo/shared",
  "main": "./dist/index.js",
  "scripts": {
    "build": "tsc"
  }
}

The API imports the compiled output, not the raw TypeScript. This is cleaner but requires running bun run build in packages/shared before committing.

Using the CLI to deploy

For one-off deploys or testing:

panda login
panda projects create \
  --repo yourorg/my-monorepo \
  --branch main \
  --name hono-api \
  --type container \
  --root-dir apps/api \
  --start-command "bun run start"

The CLI reads pandastack.json from apps/api/ if it exists, or you can pass all parameters as flags.

Comparing Bun to Node.js for Hono

Hono runs on Node.js, Bun, Deno, and Cloudflare Workers. On Bun, the startup is faster (sub-100ms vs 200-300ms for Node.js), and the HTTP server is built into the runtime (no http.createServer()).

The trade-off: Bun is less mature than Node.js. Some npm packages assume Node.js internals (like fs.promises) and break on Bun. For most Hono apps (REST APIs, GraphQL servers), Bun works fine. For apps with heavy native dependencies (image processing, PDFs), Node.js is safer.

If you switch from Bun to Node.js later, change the start command from bun run start to node src/index.js (or npm start). The rest of the deployment config stays the same.

What you've deployed

A Hono API running on Bun, deployed from a monorepo subdirectory. The build system isolates the API's dependencies, resolves workspace packages automatically, and deploys only the backend. The frontend can be deployed separately from the same repo using a different rootDir.

The API is containerised (Dockerfile optional — the platform auto-detects Bun and builds it as a container). Free-tier apps scale to zero after inactivity (sub-second cold start when traffic returns). Paid tiers run on stable nodes with no scale-to-zero.

PandaStack's container builds use rootless BuildKit in ephemeral Kubernetes pods (no host Docker socket), push images to Google Artifact Registry, and deploy via Helm. Health checks run against /health, and the app is removed from the load balancer if checks fail.

References

  • [Hono documentation](https://hono.dev/)
  • [Bun workspaces](https://bun.sh/docs/install/workspaces)
  • [PandaStack container projects](https://docs.pandastack.io/projects/containers/)
  • [PandaStack monorepo guide](https://docs.pandastack.io/projects/monorepo/)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also