How do I configure Vercel to treat a compiled index.js as a serverless function instead of a static file?

I'm working on a Vercel-hosted monorepo with the following setup:
index.ts
(using the Hono framework withhono/vercel
) is compiled usingtsup
intodist/functions/index.js
Static assets are built with
Vite
intodist/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.
What I tried:
- Changing the
tsup
output toapi/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?
Answer
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 tha might work:
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 namedindex
". Thisindex
corresponds to theindex.func
directory we created.