1

I'm working on a Vercel-hosted monorepo with the following setup:

  • index.ts (using the Hono framework with hono/vercel) is compiled using tsup into dist/functions/index.js

  • Static assets are built with Vite into dist/vite/assets

So my build structure ends up like this:

dist/
├── functions/
│   └── index.js ← this is my API handler
└── vite/
    └── assets/

My vercel.json looks like this:

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "version": 2,
  "installCommand": "pnpm install",
  "buildCommand": "pnpm turbo run build",
  "outputDirectory": "dist",
  "rewrites": [
    {
      "source": "/assets/(.*)",
      "destination": "/dist/vite/assets/$1"
    },
    {
      "source": "/(.*)",
      "destination": "/dist/functions/index.js"
    }
  ]
}

Even though I set up a rewrite to /functions/index.js, Vercel is treating the file as a static asset instead of a serverless function. The file is publicly available (e.g. site.vercel.app/functions/index.js) but it's not executed as a function.

Build result

What I tried:

  • Changing the tsup output to api/index.js instead (since Vercel treats files inside the /api directory as functions). But that didn’t work, the api/index.js file didn't show up in the Vercel deployment output. I guess that Vercel doesn’t allow emitting files outside of the declared "outputDirectory" (which is dist), so writing to ../api is being ignored.

Question:

Is there a supported way to include a compiled serverless function in the output directory and still have Vercel treat it as a serverless function?

Or, is there a way to output a file to /api at the root level (outside outputDirectory) so Vercel recognizes it?

1 Answer 1

1

Your rewrites are not working as expected because they only control routing. Vercel has already inspected your dist directory and decided that functions/index.js is a static asset before the rewrites are even considered.

Solution: Use the Vercel Build Output API Structure

The most robust and recommended solution is to make your build script create the directory structure that Vercel expects. This gives you full control over the deployment.

1. Overview of the Plan

We will modify your build process to create a .vercel/output directory with the following structure:

.vercel/output/
├── config.json              # Defines routes and function configurations
├── static/                  # Your Vite static assets will go here
│   └── assets/
│       └── ...
└── functions/               # Your serverless functions go here
    └── index.func/          # A special directory for each function
        ├── .vc-config.json  # Config for this specific function
        └── index.js         # Your compiled Hono code

2. Step-by-Step Implementation

Step 1: Update your vercel.json

First, simplify your vercel.json. The routing and function definitions will now live inside the build output itself.

JSON

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "version": 2,
  "installCommand": "pnpm install",
  "buildCommand": "pnpm turbo run build",
  "outputDirectory": ".vercel/output"
}
  • outputDirectory: We change this to .vercel/output, which is the standard for this method. All build artifacts must now go into this directory.

Step 2: Modify Your Build Scripts

Your pnpm turbo run build command now needs to orchestrate a few things: compiling the function, building the static assets, and creating the necessary configuration files.

A good way to do this is with a small shell script that runs as your main build command. Let's call it build.sh.

Update your root package.json:

JSON

// package.json
{
  "scripts": {
    "build": "sh ./build.sh"
  }
}

Now, create the build.sh script in your project root:

Bash

#!/bin/bash

# Exit immediately if a command exits with a non-zero status.
set -e

# 1. Clean up previous build output
rm -rf .vercel/output

# 2. Build the Vite static assets
# We tell Vite to output to the `static` folder within our new structure.
pnpm run build:vite

# 3. Build the Hono serverless function
# We use tsup to compile the function into the `functions` directory.
pnpm run build:function

# 4. Create the necessary Vercel config files
# This is the magic that ties everything together.
mkdir -p .vercel/output
cp vercel.config.json .vercel/output/config.json

Step 3: Configure Individual Package Builds

Now, let's configure the individual build commands (build:vite and build:function) and create the required config files.

A. Configure Vite Build (build:vite)

In your Vite app's package.json, your build script should output to the correct directory.

Update your vite.config.ts:

TypeScript

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    // This is the key change!
    outDir: '../../.vercel/output/static', 
    assetsDir: 'assets' // This keeps the /assets/ path
  },
});

B. Configure Function Build (build:function)

This is the most important part. We need to compile your TypeScript function into a specific .func directory and include a small configuration file.

Update your function's tsup.config.ts:

TypeScript

// tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  splitting: false,
  sourcemap: true,
  clean: true,
  // This is the key change!
  outDir: '.vercel/output/functions/index.func',
  format: ['esm'], // Vercel functions should be ES Modules
});

Next, inside your function's source directory (e.g., packages/api/), create a .vc-config.json file. This tells Vercel how to run the code.

JSON

// packages/api/.vc-config.json
{
  "runtime": "edge",
  "entrypoint": "index.js"
}

Your function's build process needs to copy this file into the output directory. You can add this to your function's package.json script:

JSON

// packages/api/package.json
{
  "scripts": {
    "build": "tsup && cp .vc-config.json ../../.vercel/output/functions/index.func/"
  }
}

C. Create the Main config.json

In your project root, create a file named vercel.config.json (we copy this during the build script). This file replaces your old rewrites.

JSON

// vercel.config.json
{
  "version": 3,
  "routes": [
    {
      "source": "/assets/(.*)",
      "headers": {
        "cache-control": "public, max-age=31536000, immutable"
      },
      "continue": true
    },
    {
      "handle": "filesystem"
    },
    {
      "src": "/(.*)",
      "dest": "/index"
    }
  ]
}
  • handle: "filesystem": This tells Vercel to serve any static files that match the request path (like your assets in /assets/*).

  • src: "/(.*)", dest: "/index": This is the catch-all route. It says "if no static file was found, send the request to the serverless function named index". This index corresponds to the index.func directory we created.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the answer! That helped a lot! I'm wondering though, instead of using a build.sh script to orchestrate everything manually, wouldn’t it make more sense (or at least be more maintainable) to define these build steps in a turbo.json? That way I can specify task inputs and outputs, and also control whether they should be cached or not.
+1 appreciate the answer :). Was able to solve my issue with hono + turborepo where internal/workspace packages where not being linked correctly (reported here). I've documented the full steps I took for my repo here.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.