Skip to main content
RapidDev - Software Development Agency
v0-integrationsNext.js API Route

How to Integrate MySQL with V0

To use MySQL with V0, generate your data UI in V0, then create Next.js API routes that connect to MySQL using the mysql2 npm package. Store your MySQL connection string in Vercel environment variables. The key challenge for V0 apps on Vercel is serverless connection pooling — each function invocation can spawn a new connection, so use mysql2's connection pool and consider PlanetScale or Railway for MySQL hosting with serverless-friendly connection limits.

What you'll learn

  • How to generate a data management UI with V0 that reads from and writes to MySQL
  • How to set up mysql2 with connection pooling in a Next.js App Router project
  • How to write parameterized queries that prevent SQL injection
  • How to store MySQL connection credentials securely in Vercel environment variables
  • Why serverless connection pooling behaves differently from traditional server environments
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate17 min read30 minutesDatabaseApril 2026RapidDev Engineering Team
TL;DR

To use MySQL with V0, generate your data UI in V0, then create Next.js API routes that connect to MySQL using the mysql2 npm package. Store your MySQL connection string in Vercel environment variables. The key challenge for V0 apps on Vercel is serverless connection pooling — each function invocation can spawn a new connection, so use mysql2's connection pool and consider PlanetScale or Railway for MySQL hosting with serverless-friendly connection limits.

Connecting Your V0 App to MySQL with Next.js API Routes

MySQL is the relational database most developers already have running somewhere — on a shared hosting plan, a managed cloud instance, or a legacy application server. Connecting a V0-generated Next.js app to an existing MySQL database is one of the most common integration scenarios for founders who are modernizing a data layer or building a new front-end on top of existing data. V0 handles the React UI generation side, and mysql2's Node.js package handles the database queries through Next.js API routes.

The most important architectural consideration for MySQL on Vercel is how serverless functions handle database connections. In a traditional Node.js server, you create one connection pool that persists for the life of the server process — all requests share the same connections. In Vercel's serverless environment, each function execution may be a fresh instance with no existing connection, so you can end up creating many connections simultaneously under load. MySQL has a max_connections limit (typically 100-200 on most hosting plans), and each serverless function instance can exhaust these connections quickly at scale.

The solution is using mysql2's createPool function with a reasonably small pool size (5-10 connections per instance). More sophisticated solutions include using a connection proxy like PlanetScale's HTTP driver or hosting your MySQL on Railway, PlanetScale, or a MySQL-compatible database that handles connection management for you. For moderate traffic V0 apps, connection pooling with mysql2 works reliably. The code patterns in this guide use prepared statements throughout to prevent SQL injection — an important security practice whenever user-provided data is part of a query.

Integration method

Next.js API Route

V0 generates your data display and form UI while Next.js API routes handle all MySQL queries server-side using the mysql2 package. The connection string lives in Vercel environment variables and is accessed only by server-side code. Because Vercel deploys as serverless functions, connection pooling with mysql2's createPool is essential to prevent connection exhaustion as traffic scales.

Prerequisites

  • A V0 account with a Next.js project at v0.dev
  • An accessible MySQL database — hosted on Railway, PlanetScale, Aiven, or a self-managed server with an external connection string
  • MySQL connection credentials: host, port, database name, username, and password
  • A Vercel account with your V0 project deployed and connected via GitHub
  • Basic SQL knowledge for writing SELECT, INSERT, UPDATE, and DELETE queries

Step-by-step guide

1

Generate Your Data UI in V0

Start by prompting V0 to create the React components for your data management interface. V0 is excellent at generating tables, forms, and CRUD interfaces with Tailwind CSS — describe the shape of your data and the operations you need and V0 will produce a complete, well-styled component. For a table-based view, tell V0 the column names you need to display and whether users should be able to sort, search, or filter. Specify the API endpoints your UI will call: GET /api/[resource] for fetching data, POST /api/[resource]/create for creating new records, PATCH /api/[resource]/[id] for updates, and DELETE /api/[resource]/[id] for deletions. When V0 knows the exact endpoint structure, it generates the fetch calls correctly. For forms, describe the fields with their types — text inputs, number inputs, dropdowns with specific options, date pickers. V0 will generate TypeScript interfaces matching your data shape and wire up the form with controlled React state or react-hook-form depending on complexity. Review V0's generated component carefully before moving on. Check that it handles loading states (showing a spinner while the API responds), empty states (showing a message when there are no records), and error states (displaying a helpful message when an API call fails). These three states matter for production-quality UIs and V0 usually includes them, but verify they are present. Since V0 is generating mock data for the preview, the UI will show placeholder content rather than real MySQL data. That is expected — the real data connection happens through the API routes you will create in the next steps.

V0 Prompt

Create a data table for customer records with columns: ID, Name, Email, Company, and Created Date. Add a search bar above the table that filters by name or email. Include a 'Add Customer' button that opens a slide-over panel with a form (name, email, company inputs). The table fetches from /api/customers, the form POSTs to /api/customers. Show loading skeletons while data loads.

Paste this in V0 chat

Pro tip: Ask V0 to generate TypeScript interfaces for your data types (e.g., Customer, Product) alongside the components — these interfaces will be reusable in your API routes for type-safe query results.

Expected result: V0 generates a complete CRUD interface with a data table, search functionality, and an add/edit form. The component includes loading, empty, and error states and makes fetch calls to your API routes.

2

Install mysql2 and Create a Database Client

The mysql2 package is the standard MySQL driver for Node.js and is fully compatible with Next.js. Install it by adding it to your package.json and committing — Vercel will install it automatically during the build process. Create a shared database client module at lib/db.ts that exports a connection pool. This module is imported by all your API routes and ensures a single pool is used across all routes rather than creating multiple separate pools. Using a single pool is critical for preventing connection exhaustion under load. The connection pool configuration deserves careful attention. The connectionLimit setting controls how many simultaneous connections from a single serverless function instance are allowed — set this to 5-10 for typical apps. For MySQL databases hosted on PlanetScale or Railway, the default limits are generous enough that this rarely causes issues. For a self-managed MySQL server with a low max_connections setting, you may need to reduce this further. The connection string pattern for mysql2 is a DATABASE_URL format or individual credential parameters (host, user, password, database, port). Using DATABASE_URL is cleaner for Vercel environment variables. If your MySQL host requires SSL (common on cloud-hosted databases), add ssl: { rejectUnauthorized: true } to the connection configuration — some managed MySQL services provide CA certificates that you can include for stricter verification. Always use mysql2's promise-based API (the /promise import) rather than the callback-based API. Next.js App Router API routes are async/await-based, so the promise interface integrates cleanly without callback pyramid patterns.

V0 Prompt

Add a lib/db.ts file that creates a mysql2 connection pool using DATABASE_URL from environment variables, exports the pool as the default export, and uses TypeScript. Install mysql2 in package.json.

Paste this in V0 chat

lib/db.ts
1import mysql from 'mysql2/promise';
2
3// Create a connection pool that persists across serverless function invocations
4// when the module is cached. connectionLimit prevents exhausting MySQL's max_connections.
5const pool = mysql.createPool({
6 uri: process.env.DATABASE_URL,
7 connectionLimit: 10,
8 waitForConnections: true,
9 queueLimit: 0,
10 // Enable SSL for cloud-hosted MySQL databases
11 ssl: process.env.NODE_ENV === 'production'
12 ? { rejectUnauthorized: true }
13 : undefined,
14});
15
16export default pool;

Pro tip: mysql2/promise is a separate import from mysql2 — use 'import mysql from mysql2/promise' to get the async/await API. The plain 'mysql2' import uses callbacks, which are harder to use with Next.js API routes.

Expected result: The lib/db.ts module exists and exports a connection pool. Importing it in any API route gives access to the pool without creating new connections.

3

Create CRUD API Routes

Create the Next.js API routes that your V0 UI will call to perform database operations. Place these in app/api/[resource]/ directories following Next.js App Router conventions. For the GET route (fetching records), import the pool from lib/db.ts and use pool.query() with a parameterized SQL statement. Always use parameterized queries — never string-interpolate user-provided values into SQL strings. The parameterized form passes values separately from the SQL string, which the database driver handles safely to prevent SQL injection. For pagination, use LIMIT and OFFSET in your SELECT queries rather than fetching all records. A typical pattern is to accept page and pageSize query parameters and calculate OFFSET as (page - 1) * pageSize. Return both the data array and a totalCount from a separate COUNT query so the frontend can display pagination controls. For the POST route (creating records), read the request body with request.json(), validate required fields, and use INSERT INTO with parameterized values. Return the inserted record's ID from result.insertId so the frontend can display the newly created item. For DELETE routes, use a dynamic segment in the file path: app/api/customers/[id]/route.ts. The route parameter is available via params.id. Always validate that the ID is a valid format before using it in a query — a simple parseInt check prevents accidental non-numeric values. One important consideration: mysql2 returns rows as plain JavaScript objects, but the column names from your database will be exactly as defined in MySQL — often snake_case (first_name, created_at). Your V0-generated TypeScript interfaces might use camelCase. Either adjust your SQL to alias columns (SELECT first_name AS firstName) or transform the response objects in your API route before returning them to the frontend.

V0 Prompt

Add a Next.js API route at app/api/customers/route.ts that handles GET (fetch all customers from MySQL customers table using the pool from lib/db.ts, with optional search query param) and POST (insert a new customer with name, email, company fields). Use parameterized queries.

Paste this in V0 chat

app/api/customers/route.ts
1import { NextRequest, NextResponse } from 'next/server';
2import pool from '@/lib/db';
3
4export async function GET(request: NextRequest) {
5 try {
6 const search = request.nextUrl.searchParams.get('search') || '';
7 const page = parseInt(request.nextUrl.searchParams.get('page') || '1');
8 const pageSize = 20;
9 const offset = (page - 1) * pageSize;
10
11 let query: string;
12 let params: (string | number)[];
13
14 if (search) {
15 // Parameterized LIKE query — safe against SQL injection
16 query = `
17 SELECT id, name, email, company, created_at AS createdAt
18 FROM customers
19 WHERE name LIKE ? OR email LIKE ?
20 ORDER BY created_at DESC
21 LIMIT ? OFFSET ?
22 `;
23 params = [`%${search}%`, `%${search}%`, pageSize, offset];
24 } else {
25 query = `
26 SELECT id, name, email, company, created_at AS createdAt
27 FROM customers
28 ORDER BY created_at DESC
29 LIMIT ? OFFSET ?
30 `;
31 params = [pageSize, offset];
32 }
33
34 const [rows] = await pool.query(query, params);
35 const [countRows] = await pool.query(
36 search
37 ? 'SELECT COUNT(*) AS total FROM customers WHERE name LIKE ? OR email LIKE ?'
38 : 'SELECT COUNT(*) AS total FROM customers',
39 search ? [`%${search}%`, `%${search}%`] : []
40 );
41
42 const total = (countRows as any[])[0].total;
43
44 return NextResponse.json({ customers: rows, total, page, pageSize });
45 } catch (error) {
46 console.error('MySQL query error:', error);
47 return NextResponse.json(
48 { error: 'Failed to fetch customers' },
49 { status: 500 }
50 );
51 }
52}
53
54export async function POST(request: NextRequest) {
55 try {
56 const { name, email, company } = await request.json();
57
58 if (!name || !email) {
59 return NextResponse.json(
60 { error: 'name and email are required' },
61 { status: 400 }
62 );
63 }
64
65 const [result] = await pool.query(
66 'INSERT INTO customers (name, email, company, created_at) VALUES (?, ?, ?, NOW())',
67 [name, email, company || null]
68 );
69
70 const insertId = (result as any).insertId;
71
72 return NextResponse.json(
73 { id: insertId, name, email, company },
74 { status: 201 }
75 );
76 } catch (error: any) {
77 if (error.code === 'ER_DUP_ENTRY') {
78 return NextResponse.json(
79 { error: 'A customer with this email already exists' },
80 { status: 409 }
81 );
82 }
83 console.error('MySQL insert error:', error);
84 return NextResponse.json(
85 { error: 'Failed to create customer' },
86 { status: 500 }
87 );
88 }
89}

Pro tip: The mysql2 pool returns a tuple from pool.query(): [rows, fields]. Always destructure as const [rows] = await pool.query(...) and ignore the fields element if you do not need column metadata.

Expected result: GET /api/customers returns a paginated list of customers from MySQL. POST /api/customers creates a new customer and returns the inserted record with its database-generated ID.

4

Add MySQL Connection String to Vercel

Your API routes read the DATABASE_URL environment variable to connect to MySQL. Add this to Vercel's environment configuration. Go to Vercel Dashboard → your project → Settings → Environment Variables. Add DATABASE_URL with a value in the format: mysql://username:password@hostname:3306/database_name. For cloud-hosted MySQL services: - Railway: Go to your Railway MySQL service → Connect → copy the MySQL connection URL. Railway provides DATABASE_URL directly. - PlanetScale: Go to your PlanetScale database → Connect → select 'Node.js' driver → copy the connection string. PlanetScale uses a slightly different format with SSL enforced. - Aiven: Find the connection string in the Aiven Console under Services → your MySQL service → Connection Information. - AWS RDS: Construct the URL as mysql://admin:password@your-instance.rds.amazonaws.com:3306/yourdb. Ensure RDS security groups allow connections from Vercel's IP ranges (or 0.0.0.0/0 if you rely on password auth). DATABASE_URL should not use the NEXT_PUBLIC_ prefix — it contains your database password and must remain server-only. For local development, create .env.local in your project root with DATABASE_URL pointing to your local MySQL instance (mysql://root:password@127.0.0.1:3306/mydb) or a cloud database with a development database name. Never point your local development environment at the production database. After saving the environment variable in Vercel, push a commit to trigger redeployment so the new variable takes effect.

Pro tip: If your MySQL host enforces SSL (common on cloud services), append ?ssl=true to your DATABASE_URL or configure the ssl option in mysql2's createPool call. Connections without SSL will fail on SSL-enforced hosts even with correct credentials.

Expected result: Vercel Dashboard shows DATABASE_URL saved as a server-only environment variable. After redeployment, the API routes can open MySQL connections without connection refused errors.

5

Test Queries and Handle Edge Cases

Test your MySQL integration by exercising the API routes through your V0 UI. Start with the GET route — load your data table page and confirm records appear. If no records appear, check the Vercel Function Logs (Vercel Dashboard → your project → Functions) for MySQL error messages. Common first-run issues include: the MySQL host not being accessible from Vercel's infrastructure (check firewall rules), SSL configuration mismatches, incorrect database or table names, and column name mismatches between your SQL queries and your TypeScript interfaces. For the write operations, test creating a new record through the form V0 generated. Verify the record appears in the list after creation. Then test the duplicate-entry case (if you have a UNIQUE index on email) to confirm the error handling returns a useful message rather than an unhandled 500 error. For production MySQL on Vercel, monitor your connection count. MySQL's SHOW STATUS LIKE 'Threads_connected' command shows current connections. If you see the count climbing toward your max_connections limit, reduce connectionLimit in your pool configuration or consider migrating to a MySQL-compatible database with better serverless connection handling like PlanetScale, which uses a proprietary connection multiplexer. For complex MySQL integrations with multi-table joins, stored procedures, or full-text search, the query structure can become involved. RapidDev's team can help optimize MySQL query performance and connection management for V0 apps under production load.

V0 Prompt

Add an empty state to the customers table that shows a friendly illustration and 'No customers yet. Click Add Customer to get started.' message when the API returns an empty array.

Paste this in V0 chat

Pro tip: Add a health check route at app/api/health/route.ts that runs SELECT 1 against your MySQL pool — this helps you quickly verify the database connection is working without loading the full data table.

Expected result: The data table shows real records from MySQL. Creating a new record through the form adds it to the database and it appears in the table on the next load. Connection errors show descriptive messages rather than generic 500 errors.

Common use cases

CRM Dashboard with Customer Records

A business owner uses V0 to build a customer management dashboard on top of their existing MySQL database. The dashboard displays customers in a sortable table, allows searching by name or email, and provides a form to add new customers. All data operations go through Next.js API routes that execute parameterized SQL queries safely.

V0 Prompt

Create a customer management table with columns for name, email, company, and created date. Include a search input that filters results as the user types. Add a 'New Customer' button that opens a form modal with name, email, and company fields. The table should fetch data from /api/customers and the form should POST to /api/customers/create.

Copy this prompt to try it in V0

Inventory Management System

A retail store owner uses V0 to build a product inventory dashboard connected to their MySQL product database. Products display with current stock counts, low-stock alerts highlight items below a threshold, and staff can update quantities through a simple inline edit. Changes are written back to MySQL via API routes.

V0 Prompt

Build an inventory table showing product name, SKU, current stock, and reorder point. Highlight rows in yellow when stock is below reorder point and red when stock is zero. Include an edit icon on each row that opens an inline quantity update field. Fetch data from /api/inventory and update via PATCH to /api/inventory/{id}.

Copy this prompt to try it in V0

Blog or Content Management Interface

A content creator uses V0 to build a simple CMS dashboard on their MySQL-backed blog database. Posts display in a list with title, publish date, and status. The editor can create new posts, edit existing ones, and toggle publish status — all stored in MySQL through server-side API routes with proper input validation.

V0 Prompt

Create a blog post management list with columns for title, status badge (published/draft), publish date, and action buttons for Edit and Delete. Add a 'New Post' button that navigates to a /posts/new page with title, content (textarea), and status fields. Fetch posts from /api/posts.

Copy this prompt to try it in V0

Troubleshooting

API route logs 'ECONNREFUSED' or 'connect ETIMEDOUT' when connecting to MySQL

Cause: Vercel's serverless functions cannot reach the MySQL host. This happens when the MySQL server is not publicly accessible, the port (default 3306) is blocked by a firewall, or the hostname in DATABASE_URL is incorrect.

Solution: Verify your MySQL server allows inbound connections from external IPs. For cloud hosts, check the firewall or security group rules. For local MySQL, you cannot connect from Vercel — use a cloud-hosted MySQL service like Railway, PlanetScale, or Aiven. Confirm the DATABASE_URL hostname, port, and credentials are correct.

Too many connections error from MySQL: ER_CON_COUNT_ERROR

Cause: Multiple Vercel serverless function instances are each opening multiple connections, exhausting MySQL's max_connections limit (typically 100-200 on shared hosting plans).

Solution: Reduce the connectionLimit in your mysql2 pool configuration (try 3-5). Consider using PlanetScale or a MySQL proxy that handles connection multiplexing. For high-traffic apps, evaluate migrating to Neon or Supabase which are purpose-built for serverless connection patterns.

typescript
1// Reduce pool size to avoid exhausting MySQL connections
2const pool = mysql.createPool({
3 uri: process.env.DATABASE_URL,
4 connectionLimit: 5, // Reduce from 10 to 5
5 waitForConnections: true,
6});

mysql2 returns 'ER_ACCESS_DENIED_ERROR' in Vercel logs but connection works locally

Cause: The database user in your DATABASE_URL does not have permission to connect from the Vercel IP range, or the Vercel environment variable has a different password than your local .env.local file.

Solution: In Vercel Dashboard, verify DATABASE_URL is set correctly for the Production environment (not just Development). Check that the MySQL user is granted access from '%' (any host) or from Vercel's IP range. For RDS, the security group must allow inbound port 3306 from 0.0.0.0/0 or specifically from Vercel's IP ranges.

TypeScript error: 'Property X does not exist on type RowDataPacket[]'

Cause: mysql2 returns query results typed as RowDataPacket[] which TypeScript does not automatically map to your custom interface. Type assertions are needed to work with the results.

Solution: Use type assertions to cast the query results to your custom type. You can also use the generic pool.query<YourType[]>() syntax to specify the expected return type.

typescript
1// Type the result correctly
2const [rows] = await pool.query<mysql.RowDataPacket[]>('SELECT * FROM customers');
3const customers = rows as Customer[]; // Your custom interface

Best practices

  • Always use parameterized queries with ? placeholders — never concatenate user input directly into SQL strings, as this creates SQL injection vulnerabilities.
  • Store DATABASE_URL without the NEXT_PUBLIC_ prefix so the connection string and credentials are never exposed in browser-side code.
  • Use mysql2's promise-based API (mysql2/promise) throughout — the callback-based API leads to nested callback patterns that are harder to maintain in Next.js API routes.
  • Set an appropriate connectionLimit in your pool (5-10 for typical apps) to prevent exhausting MySQL's max_connections at scale.
  • For cloud-hosted MySQL on Railway, PlanetScale, or Aiven, ensure SSL is configured — most cloud providers enforce SSL connections to protect data in transit.
  • Add database indexes on columns you frequently filter or sort by (email for user lookups, created_at for date-sorted lists) — unindexed queries become slow as your data grows.
  • Handle MySQL error codes explicitly in catch blocks (ER_DUP_ENTRY for unique constraint violations, ER_ROW_IS_REFERENCED for foreign key constraints) rather than returning generic 500 errors.

Alternatives

Frequently asked questions

Can I connect V0 apps to an existing MySQL database I already have?

Yes — if your existing MySQL database is accessible from the internet (not just on a local network), you can connect to it by setting DATABASE_URL in your Vercel environment variables. The database just needs to be on a host that allows inbound connections from external IPs. Shared hosting MySQL databases that only allow local connections will not work directly — in that case, consider replicating data to a cloud-hosted MySQL instance.

Should I use mysql2 or Prisma ORM for my V0 app?

For simple CRUD operations with a few tables, direct mysql2 queries give you more control and are easier to debug. For more complex applications with many related tables, Prisma ORM provides type-safe query building, auto-generated TypeScript types from your schema, and built-in connection pooling. Prisma has a steeper initial setup but reduces query-writing errors in larger codebases. V0 can generate code using either approach.

Why does my MySQL connection work locally but fail on Vercel?

The most common reason is firewall restrictions — your local machine is on a different IP than Vercel's servers. Check that your MySQL host allows connections from all IPs (0.0.0.0/0) or specifically from Vercel's IP ranges. Cloud MySQL services often have network access controls that default to allowing only specific IPs. Another common reason is SSL — many cloud MySQL hosts require SSL connections, which your local config might not enforce but Vercel's environment does.

What MySQL hosting service works best with Vercel?

Railway and PlanetScale are the most commonly recommended MySQL hosts for Vercel deployments. Railway has straightforward setup, automatic backups, and a DATABASE_URL that works directly with mysql2. PlanetScale offers a MySQL-compatible serverless platform with a built-in connection multiplexer that handles the serverless connection pooling problem elegantly, though it has removed foreign key constraint support and discontinued its free tier.

Can V0 generate database schema SQL for my MySQL tables?

V0 can generate CREATE TABLE statements and database schema SQL when you describe your data model in the prompt. For example: 'Generate a MySQL schema for a customers table with id, name, email, company, and created_at columns.' The generated SQL will be syntactically correct for MySQL and can be run in your MySQL client or through a migration tool.

How do I handle MySQL migrations in a V0 Next.js project?

For simple projects, maintain a migrations/ directory with numbered SQL files and run them manually on your MySQL host. For more structured migration management, Flyway (Java-based) or golang-migrate (installable as a binary) can manage MySQL migrations. If you are using Prisma ORM with MySQL, Prisma Migrate handles schema changes with automatic migration file generation. V0 does not automatically manage database migrations — this step is always manual.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.