Next.js_
Connect a Next.js App Router application to an Appwrite native MySQL database from Route Handlers, Server Actions, and Server Components, pool from serverless, and fall back to the SQL API on the Edge runtime.
5 min read
An Appwrite native MySQL database works with standard MySQL drivers and ORMs, so a Next.js App Router application can query it from server-side code. Point your driver at the connection string from the Connections page and keep all database access on the server.
You'll need a native MySQL database in a ready state and its credentials. See MySQL to create one and Connections to retrieve the connection string. The primary user is admin, and Appwrite generates the database name for each database.
Where to connect
In the App Router, every server-side execution context runs on the Node.js runtime by default, and the Node.js runtime can open raw TCP sockets. That means you can use a standard database driver from any of these:
- Route Handlers (
app/api/.../route.ts) for public routes, webhooks, and REST-style APIs. - Server Actions (
'use server'functions) for form submissions and app-internal mutations. - Server Components (
asynccomponents) for read queries that render straight into the page.
Never import a database driver into a Client Component ('use client') or ship the connection string to the browser. Keep all database access on the server.
The one exception is the Edge runtime (export const runtime = 'edge'), which runs in a constrained environment and cannot use Node.js TCP drivers. If a route opts into Edge, use the SQL API over HTTPS, see the Edge runtime section below.
Install the driver
Install the MySQL driver for Node.js:
npm install mysql2Environment variables
Put the connection strings in your environment and never commit them. For local development, use .env.local, which Next.js loads automatically and the default create-next-app template excludes from Git:
DATABASE_URL="mysql://admin:<password>@db-<hash>.<region>.appwrite.center:6033/<database>"DIRECT_URL="mysql://admin:<password>@db-<hash>.<region>.appwrite.center:3306/<database>"MYSQL_SSL="true"DATABASE_URL points at the connection pooler port (6033) for runtime traffic, and DIRECT_URL points at the MySQL engine port (3306) for migrations and schema changes. The connection string returned by Appwrite uses the engine port, so change only the port when you connect through the pooler. If your specification does not include the pooler, use the direct connection string for DATABASE_URL too.
Appwrite Cloud serves MySQL over TLS. The examples below use MYSQL_SSL="true" to make mysql2 request TLS and verify the server certificate with the runtime's trusted CAs. For full certificate verification controls or mTLS, see the Network security page.
Create a table for the examples
Run this SQL once through your migration workflow or the mysql client:
CREATE TABLE IF NOT EXISTS nextjs_users ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);
INSERT INTO nextjs_users (email)VALUES ('ada@example.com')ON DUPLICATE KEY UPDATE email = nextjs_users.email;Pool serverless connections
When you deploy to Vercel, Netlify, or another serverless platform, each invocation can spin up a fresh instance with its own connection pool. Hundreds of concurrent invocations fan out into far more backend connections than the engine allows. Route runtime traffic through the connection pooler on the pooler port so it can multiplex those instances over a small number of backend connections.
The pooler defaults to transaction mode, which does not keep a backend connection across statements, so session-level features such as server-side prepared statements and temporary tables are unavailable. Use pool.query() with ? placeholders for pooled runtime queries. Reserve the engine port for migrations, schema changes, and workloads that need a full session.
Use a singleton client
Instantiate one pool per module scope and reuse it across invocations, so warm serverless instances do not reconnect on every request. In development, Next.js hot reload re-evaluates modules, which can leak connections, so cache the pool on globalThis.
With mysql2:
// lib/db.tsimport mysql, { type Pool } from 'mysql2/promise';
const globalForDb = globalThis as unknown as { mysqlPool?: Pool };
function createPool() { const url = new URL(process.env.DATABASE_URL!);
return mysql.createPool({ host: url.hostname, port: Number(url.port || 3306), user: decodeURIComponent(url.username), password: decodeURIComponent(url.password), database: decodeURIComponent(url.pathname.slice(1)), waitForConnections: true, connectionLimit: 5, ssl: process.env.MYSQL_SSL === 'true' ? { rejectUnauthorized: true } : undefined });}
export const pool = globalForDb.mysqlPool ?? createPool();
if (process.env.NODE_ENV !== 'production') globalForDb.mysqlPool = pool;Query it from a Server Component or Route Handler:
// app/users/route.tsimport type { RowDataPacket } from 'mysql2';
import { pool } from '@/lib/db';
type UserRow = RowDataPacket & { id: number; email: string; created_at: Date;};
export async function GET() { const [users] = await pool.query<UserRow[]>( 'SELECT id, email, created_at FROM nextjs_users ORDER BY created_at DESC LIMIT ?', [10] );
return Response.json(users);}Use Prisma or Drizzle
For a typed schema, migrations, and a query builder, reach for an ORM. Both integrate with the pooled DATABASE_URL plus direct DIRECT_URL pattern above.
- Prisma: point the runtime at the pooled connection string and keep the Prisma CLI on the engine port for migrations, then run
prisma migrate deploy. See the Prisma guide for the full config and migration flow. - Drizzle: use Drizzle's MySQL driver for runtime queries and the direct URL for
drizzle-kitmigrations. See the Drizzle guide.
Edge runtime: use the SQL API
If a Route Handler or route segment opts into the Edge runtime, it cannot use a Node.js MySQL driver:
export const runtime = 'edge';From the Edge runtime, you can execute one parameterized SQL statement over HTTPS and get JSON back. SELECT, INSERT, UPDATE, and DELETE statements are allowed by default. Use the global fetch available in the Edge runtime:
// app/edge-users/route.tsexport const runtime = 'edge';
export async function GET() { const response = await fetch( 'https://<REGION>.cloud.appwrite.io/v1/mysql/<DATABASE_ID>/executions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Appwrite-Project': process.env.APPWRITE_PROJECT_ID!, 'X-Appwrite-Key': process.env.APPWRITE_API_KEY! }, body: JSON.stringify({ sql: 'SELECT id, email FROM nextjs_users WHERE created_at > ? ORDER BY created_at DESC LIMIT ?', bindings: ['2026-05-01 00:00:00', 10] }) } );
if (!response.ok) { throw new Error(await response.text()); }
const { rows } = await response.json(); return Response.json(rows);}The response is { rows, rowCount, columns, durationMs, truncated, bytes }. Bindings are sent separately and never interpolated into the SQL string. Add APPWRITE_PROJECT_ID and APPWRITE_API_KEY to your environment alongside the database URLs. Use an API key with the dedicatedDatabases.execute scope.
Local development
next dev runs on the Node.js runtime, so a local server can connect to the native MySQL database with the same server-only driver code. Keep DATABASE_URL, DIRECT_URL, and MYSQL_SSL in .env.local.
For throwaway data in tests or experiments, create a branch, an instant, isolated copy with its own connection string, and point .env.local at it. Delete the branch when you're done.
Deploy
When you deploy, set the same DATABASE_URL, DIRECT_URL, MYSQL_SSL, APPWRITE_PROJECT_ID, and APPWRITE_API_KEY as environment variables on your hosting platform. Run migrations from the direct connection in your build or release step, and use the SQL API from any Edge Functions.
Related
Was this page helpful?
Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.