Build & Deploy a Full-Stack Monorepo (Turborepo) : Vite/React + Fastify + Prisma on Vercel Serverless

Nithin RajNithin Raj
8 min read

Introduction: The Monorepo Maze & The Serverless Prize

Building full-stack apps with separate frontend and backend codebases is standard, but managing them can become cumbersome. Monorepos, powered by tools like Turborepo and pnpm, offer a streamlined solution. Combining this with a modern stack like Vite/React/TypeScript for the frontend and Fastify/TypeScript for a performant backend API seems ideal.

But how do you deploy this elegantly? While traditional VPS hosting works, Vercel's Serverless Functions offer auto-scaling, zero server management, and cost-effectiveness. Getting a Fastify backend running as a Vercel function within a Turborepo setup, however, isn't always straightforward.

This guide details the journey, starting with the with-vite-react Turborepo template, integrating Prisma with Accelerate for database access, and critically, configuring Vercel for a successful serverless deployment. We'll explicitly cover the errors encountered along the way (like ESM/CJS conflicts and build output issues) and show you the exact configurations that work.

By the end of this post, you'll have a deployable template and understand the key configurations needed to make Vite, Fastify, Prisma, and Vercel Serverless play nicely together within a Turborepo.

Here is the final version of the code checkout the code from below github URL

GitHub Code → Github URL

Prerequisites

  • Node.js (v18+ recommended)

  • pnpm (npm install -g pnpm)

  • Git & GitHub Account

  • Vercel Account

  • PostgreSQL Database & Prisma Data Platform Account (for Accelerate connection string - Optional)

The Stack

  • Monorepo: Turborepo + pnpm Workspaces

  • Frontend: Vite + React + TypeScript (apps/web)

  • Backend: Fastify + TypeScript (apps/backend)

  • Database: Prisma ORM + Prisma Accelerate(Optional)

Step 1: Initialization - The Starting Line

We begin with the specific Turborepo template:

pnpm dlx create-turbo@latest -e with-vite-react my-app
cd my-app
pnpm install

This gives us a solid foundation with apps/web (Vite/React) and various packages/* for shared configs/UI. We'll add our apps/backend next.

Folder Structure Overview

.
├── api
│   └── index.js
├── apps
│   ├── backend
│   │   ├── package.json
│   │   ├── prisma
│   │   │   └── schema.prisma
│   │   ├── src
│   │   │   ├── index.ts
│   │   │   ├── lib
│   │   │   │   └── prisma.ts
│   │   │   └── routes
│   │   │       ├── index.ts
│   │   │       └── todo.ts
│   │   └── tsconfig.json
│   └── web
│       ├── index.html
│       ├── package.json
│       ├── public
│       │   ├── typescript.svg
│       │   └── vite.svg
│       ├── src
│       │   ├── components
│       │   │   ├── TodoList.d.ts
│       │   │   ├── TodoList.d.ts.map
│       │   │   ├── TodoList.js
│       │   │   └── TodoList.tsx
│       │   ├── main.d.ts
│       │   ├── main.d.ts.map
│       │   ├── main.js
│       │   ├── main.tsx
│       │   ├── style.css
│       │   └── vite-env.d.ts
│       ├── tsconfig.json
│       └── vite.config.ts
├── package.json
├── packages
│   ├── eslint-config
│   │   ├── index.js
│   │   └── package.json
│   ├── typescript-config
│   │   ├── base.json
│   │   ├── package.json
│   │   ├── react-library.json
│   │   └── vite.json
│   └── ui
│       ├── components
│       │   ├── counter.tsx
│       │   ├── header.tsx
│       │   └── index.ts
│       ├── index.ts
│       ├── package.json
│       └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── turbo.json
└── vercel.json

Step 2: Building the Fastify + Prisma Backend

  1. Create apps/backend that is and initialize pnpm init. Add "type": "module" to its package.json.

  2. Install Dependencies: fastify, @fastify/cors, @fastify/sensible, prisma, @prisma/client, and dev dependencies (typescript, @types/node, @repo/*, tsc-watch, etc.)

     pnpm add fastify, @fastify/cors, @fastify/sensible, prisma, @prisma/client
    
     pnpm add -D typescript, @types/node, @repo/*, tsc-watch
    
  3. Configure apps/backend/tsconfig.json for ESM output ("module": "NodeNext").

  4. Setup Prisma:

     cd apps/backend
    
     pnpm dlx prisma init
    
    • Configure prisma/schema.prisma for Accelerate Define your Todo model.

        model Todo {
          id        String   @id @default(cuid())
          title     String
          completed Boolean  @default(false)
          createdAt DateTime @default(now())
          updatedAt DateTime @updatedAt
        }
      
    • Add DATABASE_URL to apps/backend/.env and .gitignore. Add it to Vercel Environment Variables.

    • Modify apps/backend/package.json build script: "build": "prisma generate && tsc".

    • Create apps/backend/src/lib/prisma.ts for the singleton Prisma client.

    • Push the schema: pnpm dlx prisma db push

  5. Implement Fastify App (apps/backend/src/index.ts):

    • Create the createApp function, register plugins (CORS, Sensible).

    • Use conditional logging (no pino-pretty in production).

    • Register routes. Add /health endpoint. Export createApp.

  6. Implement Todo Routes (apps/backend/src/routes/todo.ts): Use the imported prisma client for database operations (findMany, create, etc.).

Step 3: Frontend Integration - The TodoList Component

Our apps/web contains a React component, likely named TodoList.tsx. Its core job is to interact with our backend API:

// Simplified snippet from apps/web/src/components/TodoList.tsx
import React, { useState, useEffect } from 'react';

// Base URL for API calls - uses Vite env var for flexibility
const API_URL = import.meta.env.VITE_API_URL || '/api';

// ... (Todo interface) ...

export const TodoList: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  // ... (state for new todo title) ...

  const fetchTodos = async () => {
    setLoading(true);
    setError(null);
    try {
      // Fetching from the /api path Vercel will rewrite
      const response = await fetch(`${API_URL}/todos`);
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      const data = await response.json();
      setTodos(data);
    } catch (err: any) {
      setError(err.message || 'Failed to fetch todos');
      console.error("Fetch error:", err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { fetchTodos(); }, []);

  // ... (handleAddTodo, handleToggleComplete, handleDeleteTodo functions
  //      all using fetch with `${API_URL}/todos/...` and appropriate methods/bodies) ...

  // ... (JSX rendering list, form, loading/error states) ...
};

Key points:

  • It uses fetch to make requests to paths starting with /api (e.g., /api/todos, /api/health).

  • It manages loading and error states for a better user experience.

  • It relies on Vercel to correctly route these /api/* requests to our backend function.

Step 4: Vercel Deployment - Demystifying the Config

Here's where things get interesting. Deploying a monorepo with a separate backend API to Vercel Serverless requires careful configuration. Many initial attempts fail due to misunderstandings about how Vercel builds and routes.

Common Pitfalls & Solutions:

  1. Incorrect Output Directory: Setting the Vercel UI "Output Directory" to the root dist (or overriding it in vercel.json) was a problem. Vercel's Turborepo integration needs to handle this.

    • Solution: Leave the "Output Directory" setting blank in the Vercel UI. Vercel will automatically detect and serve the static assets from apps/web/dist.
  2. Incorrect Build Command: Using custom scripts like vercel-build that manually copy files can break Vercel's function detection.

    • Solution: Let Vercel use the standard build command. Ensure your root package.json has "build": "turbo run build". In the Vercel UI, leave "Build Command" blank or override it with pnpm build.
  3. ESM/CJS Module Conflicts: Our backend is ESM ("type": "module"), but the Vercel Node.js runtime for the api/index.js wrapper might default to CJS. Using require in CJS to load an ESM module (or vice-versa) fails.

    • Solution: Use dynamic import() inside the api/index.js wrapper (which is treated as CJS since we removed its package.json).
  4. Prisma Client Not Found: The serverless function crashing because @prisma/client wasn't initialized.

    • Solution: Explicitly run prisma generate during the build (add it to the backend's build script) AND ensure the generated client and schema are included in the function bundle via includeFiles in vercel.json.
  5. API Path Mismatch: Vercel routes /api/health to the function, but Fastify expects /health.

    • Solution: Strip the /api prefix from req.url within the api/index.js wrapper before passing the request to the Fastify instance.

The Correct Configuration:

A) API Entry Point (api/index.js - Root Level):
The bridge between Vercel and your Fastify app.

// api/index.js 
module.exports = async (req, res) => {
  try {
    // Dynamic import for ESM backend build
    const { default: createApp } = await import('../apps/backend/dist/index.js');
    const app = createApp();
    await app.ready();

    // Strip /api prefix before passing to Fastify
    const fullUrl = new URL(req.url, `http://${req.headers.host}`);
    const originalPathname = fullUrl.pathname;
    const pathForFastify = originalPathname.startsWith('/api')
                           ? originalPathname.substring(4) || '/'
                           : originalPathname;
    req.url = pathForFastify + fullUrl.search; // Keep query params!

    app.server.emit('request', req, res); // Pass modified req to Fastify
  } catch (error) {
    console.error("Vercel Function Error:", error);
    res.statusCode = 500;
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ error: "Internal Server Error", message: error.message }));
  }
};

B) Vercel Configuration (vercel.json - Root Level):
This file instructs Vercel how to build and route.

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "version": 2,
  // NO root buildCommand or outputDirectory - Let Vercel handle it!
  "functions": {
    "api/index.js": { // Defines our serverless function
      "memory": 1024, // MB - Adjust if needed
      "maxDuration": 15, // Seconds - Increase for potentially long tasks/cold starts
      "includeFiles":  "apps/backend/dist/**",
    }
  },
  "rewrites": [
    { // Route API requests to the function
      "source": "/api/(.*)",         // Matches any path starting with /api/
      "destination": "/api/index.js" // Points to our entry point file
    }
    // If using React Router for frontend routing, add an SPA fallback:
    // { "source": "/((?!api/).*)", "destination": "/index.html" }
  ]
}

C) Vercel Project Settings (Dashboard UI):

  • Framework Preset: Other

  • Root Directory: ./

  • Build Command: (Leave blank - let Vercel override)

  • Output Directory: (Leave blank!)

  • Install Command: (Leave blank - let Vercel override)

  • Environment Variables: Add DATABASE_URL.

Step 7: Deploy & Celebrate!

  1. Commit the final code (api/index.js, vercel.json, backend changes, frontend changes).

  2. Push to GitHub.

  3. Watch the Vercel deployment. Verify the build logs show Turborepo building both web and backend, and the function compilation.

  4. Test your application! The frontend should load, and API calls to /api/todos, /api/health, etc., should now correctly interact with your Fastify backend running serverlessly and persisting data via Prisma Accelerate. Check the Vercel Function logs if you encounter runtime issues.

Conclusion

Deploying a full-stack Turborepo monorepo to Vercel Serverless, especially with Prisma, requires understanding Vercel's build process and correctly configuring the bridge between the serverless runtime and your backend framework. By letting Vercel handle the core Turborepo build, carefully defining your serverless function (vercel.json, including includeFiles), and using a wrapper (api/index.js) to manage module types and paths, you achieve a scalable, efficient, and maintainable deployment. You've conquered the maze!

Reach Me Out

If you found this post helpful or want to check out the full codebase, feel free to explore the project on GitHub:

GitHub Repository: nithin-raj-9100/vite_react_fastityf_ts

Feel free to star ⭐ the repo or open an issue if you have any questions. Happy coding!

10
Subscribe to my newsletter

Read articles from Nithin Raj directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nithin Raj
Nithin Raj