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

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

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.jsthis 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?

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 named index". This index corresponds to the index.func directory we created.

Related Articles