Update with-mysql example to Next.js 15, Tailwind 4, Prisma 7 (#88475)

## Summary

Updates the `with-mysql` example to use latest versions of all
dependencies.

### What

- Migrate from Pages Router to App Router
- Update to React 19 and Next.js latest
- Update to Tailwind CSS v4 with `@import "tailwindcss"` syntax
- Update to Prisma 7 with `@prisma/adapter-planetscale` driver adapter
- Convert all files to TypeScript
- Remove unused API route and `vercel.svg`
- Update README to clarify this is for PlanetScale MySQL and link to
[vercel/postgres-next-starter](https://github.com/vercel/postgres-next-starter)
for Postgres users

### Why

The example was significantly outdated:
- Prisma 3.10.0 → Prisma 7 (latest)
- Tailwind 3.0.23 → Tailwind 4
- React 18.2.0 → React 19
- Pages Router → App Router

### How

Followed the official migration guides:
- [Prisma 7 upgrade
guide](https://www.prisma.io/docs/orm/more/upgrade-guides/upgrading-versions/upgrading-to-prisma-7)
- [PlanetScale quickstart with
Prisma](https://www.prisma.io/docs/getting-started/prisma-orm/quickstart/planetscale)
- Aligned configuration files with a fresh `create-next-app` output

## Related

N/A

## How to Test

```bash
cd examples/with-mysql
npm install
# Set up .env with your PlanetScale DATABASE_URL
npx prisma generate
npx prisma db push
npx prisma db seed
npm run dev
```

## Checklist

- [x] Tests and samples are included
- [x] Documentation is updated (README)

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
This commit is contained in:
Simeon Griggs
2026-01-20 12:16:44 +01:00
committed by GitHub
co-authored by vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
parent 2ed21f26d3
commit 70d357a3ea
26 changed files with 286 additions and 254 deletions
+6 -1
View File
@@ -28,8 +28,10 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# env files
.env
.env*.local
# vercel
@@ -38,3 +40,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# prisma
/lib/generated/
+33 -40
View File
@@ -1,6 +1,8 @@
# Next.js + MySQL
# Next.js + PlanetScale MySQL
This is a [Next.js](https://nextjs.org/) project that uses [Prisma](https://www.prisma.io/) to connect to a [PlanetScale](https://planetscale.com/) MySQL database and [Tailwind CSS](https://tailwindcss.com/) for styling.
This is a [Next.js](https://nextjs.org/) project that uses [Prisma](https://www.prisma.io/) to connect to a [PlanetScale MySQL database](https://planetscale.com/) and [Tailwind CSS](https://tailwindcss.com/) for styling.
> **Using PlanetScale Postgres?** Check out the [Next.js and Postgres Starter Template](https://github.com/vercel/postgres-next-starter) for a PostgreSQL-based template.
## Demo
@@ -16,24 +18,27 @@ https://next-mysql.vercel.app
pscale auth login
```
## Set up the database
## Set Up the Database
Create a new database with the following command:
```sh
pscale database create <DATABASE_NAME>
pscale database create <DATABASE_NAME> --engine mysql
```
> A branch, `main`, was automatically created when you created your database, so you can use that for `BRANCH_NAME` in the steps below.
## Set up the starter Next.js app
## Set Up the Starter Next.js App
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
Run [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with your preferred package manager to bootstrap the example:
```bash
npx create-next-app --example with-mysql nextjs-mysql
```
<details>
<summary>Or use yarn / pnpm / bun</summary>
```bash
yarn create next-app --example with-mysql nextjs-mysql
```
@@ -42,6 +47,12 @@ yarn create next-app --example with-mysql nextjs-mysql
pnpm create next-app --example with-mysql nextjs-mysql
```
```bash
bunx create-next-app --example with-mysql nextjs-mysql
```
</details>
Next, you'll need to create a database username and password through the CLI to connect to your application. If you'd prefer to use the dashboard for this step, you can find those instructions in the [Connection Strings documentation](https://docs.planetscale.com/concepts/connection-strings#creating-a-password) and then come back here to finish setup.
First, create your `.env` file by renaming the `.env.example` file to `.env`:
@@ -61,12 +72,12 @@ pscale password create <DATABASE_NAME> <BRANCH_NAME> <PASSWORD_NAME>
Take note of the values returned to you, as you won't be able to see this password again.
```text
Password production-password was successfully created.
Password <PASSWORD_NAME> was successfully created in <DIRECTORY_NAME>.
Please save the values below as they will not be shown again
NAME USERNAME ACCESS HOST URL ROLE PLAIN TEXT
--------------------- -------------- ----------------------------------- ------------------ -------------------------------------------------------
production-password xxxxxxxxxxxxx xxxxxx.us-east-2.psdb.cloud Can Read & Write pscale_pw_xxxxxxx
NAME USERNAME ACCESS HOST URL ROLE PLAIN TEXT
---------------- -------------- ----------------------------- ----------------- -----------------------------
<PASSWORD_NAME> xxxxxxxxxxxxx xxxxxx.us-east-2.psdb.cloud Can Read & Write pscale_pw_xxxxxxx
```
You'll use these properties to construct your connection string, which will be the value for `DATABASE_URL` in your `.env` file. Update the `DATABASE_URL` property with your connection string in the following format:
@@ -75,53 +86,35 @@ You'll use these properties to construct your connection string, which will be t
mysql://<USERNAME>:<PLAIN_TEXT_PASSWORD>@<ACCESS_HOST_URL>/<DATABASE_NAME>?sslaccept=strict
```
Push the database schema to your PlanetScale database using Prisma.
Generate the Prisma Client:
```bash
npx prisma generate
```
Push the database schema to your PlanetScale database using Prisma:
```bash
npx prisma db push
```
```bash
yarn prisma db push
```
Run the seed script to populate your database with `Product` and `Category` data:
```bash
pnpm prisma
```
Run the seed script to populate your database with `Product` and `Category` data.
```bash
npm run seed
```
```bash
yarn seed
```
```bash
pnpm seed
npx prisma db seed
```
## Run the App
Run the app with following command:
Run the app with the following command:
```bash
npm run dev
```
```bash
yarn dev
```
Open your browser at [localhost:3000](http://localhost:3000) to see the running application.
```bash
pnpm dev
```
Open your browser at [localhost:3000](localhost:3000) to see the running application.
## Deploy your own
## Deploy Your Own
After you've got your application running locally, it's time to deploy it. To do so, you'll need to promote your database branch (`main` by default) to be the production branch ([read the branching documentation for more information](https://docs.planetscale.com/concepts/branching)).
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+19
View File
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "PlanetScale MySQL + Next.js",
description: "A Next.js app using Prisma with PlanetScale MySQL",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
+24
View File
@@ -0,0 +1,24 @@
import prisma from "@/lib/prisma";
import { Product } from "@/components/Product";
export default async function Home() {
const products = await prisma.product.findMany({
include: {
category: true,
},
});
return (
<main className="p-10 mx-auto max-w-4xl">
<h1 className="text-6xl font-bold mb-4 text-center">Next.js Starter</h1>
<p className="mb-20 text-xl text-center">
Shop from the hottest items in the world
</p>
<div className="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 justify-items-center gap-4">
{products.map((product) => (
<Product key={product.id} product={product} />
))}
</div>
</main>
);
}
-31
View File
@@ -1,31 +0,0 @@
import Image from "next/image";
export default function Product({ product }) {
const { name, description, price, image, category } = product;
return (
<div
className="max-w-[250px] rounded overflow-hidden shadow-lg"
key={product.id}
>
<Image
className="w-full"
width={250}
height={250}
objectFit="cover"
src={image}
alt={name}
/>
<div className="px-6 py-4">
<div className="font-bold text-xl mb-2">{name}</div>
<p className="text-gray-700 text-base">{description}</p>
<p className="text-gray-900 text-xl">${price}</p>
</div>
<div className="px-6 pt-4 pb-2">
<span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
{category.name}
</span>
</div>
</div>
);
}
@@ -0,0 +1,37 @@
import Image from "next/image";
import type {
Product as ProductType,
Category,
} from "@/lib/generated/prisma/client";
type ProductWithCategory = ProductType & {
category: Category | null;
};
export function Product({ product }: { product: ProductWithCategory }) {
const { name, description, price, image, category } = product;
return (
<div className="max-w-[250px] rounded overflow-hidden shadow-lg">
<Image
className="w-full object-cover"
width={250}
height={250}
src={image}
alt={name}
/>
<div className="px-6 py-4">
<div className="font-bold text-xl mb-2">{name}</div>
<p className="text-gray-700 text-base">{description}</p>
<p className="text-gray-900 text-xl">${price.toString()}</p>
</div>
<div className="px-6 pt-4 pb-2">
{category && (
<span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
{category.name}
</span>
)}
</div>
</div>
);
}
-14
View File
@@ -1,14 +0,0 @@
import { PrismaClient } from "@prisma/client";
let prisma;
if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient();
} else {
if (!global.prisma) {
global.prisma = new PrismaClient();
}
prisma = global.prisma;
}
export default prisma;
+21
View File
@@ -0,0 +1,21 @@
import { PrismaPlanetScale } from "@prisma/adapter-planetscale";
import { PrismaClient } from "./generated/prisma/client";
import { fetch as undiciFetch } from "undici";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
function createPrismaClient() {
const adapter = new PrismaPlanetScale({
url: process.env.DATABASE_URL,
fetch: undiciFetch,
});
return new PrismaClient({ adapter });
}
const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;
-4
View File
@@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
};
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+19 -8
View File
@@ -1,21 +1,32 @@
{
"type": "module",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"seed": "node prisma/seed.js"
"lint": "next lint"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "3.10.0",
"@prisma/adapter-planetscale": "latest",
"@prisma/client": "latest",
"next": "latest",
"react": "18.2.0",
"react-dom": "18.2.0"
"react": "^19.0.0",
"react-dom": "^19.0.0",
"undici": "^7"
},
"devDependencies": {
"autoprefixer": "10.4.2",
"postcss": "8.4.7",
"prisma": "3.10.0",
"tailwindcss": "3.0.23"
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"dotenv": "^16",
"prisma": "latest",
"tailwindcss": "^4",
"tsx": "^4",
"typescript": "^5"
}
}
-5
View File
@@ -1,5 +0,0 @@
import "../styles/globals.css";
export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
-16
View File
@@ -1,16 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import prisma from "./../../lib/prisma.js";
export default async function handler(req, res) {
if (req.method === "GET") {
try {
const data = await prisma.product.findMany({});
return res.status(200).json({ data });
} catch (err) {
console.error(err);
return res.status(500).json({ msg: "Something went wrong" });
}
} else {
return res.status(405).json({ msg: "Method not allowed" });
}
}
-46
View File
@@ -1,46 +0,0 @@
import Head from "next/head";
import Product from "../components/Product";
import prisma from "../lib/prisma";
export default function Home({ products }) {
return (
<div>
<Head>
<title>PlanetScale Next.js Quickstart</title>
<meta name="description" content="PlanetScale Quickstart for Next.js" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="p-10 mx-auto max-w-4xl">
<h1 className="text-6xl font-bold mb-4 text-center">Next.js Starter</h1>
<p className="mb-20 text-xl text-center">
🔥 Shop from the hottest items in the world 🔥
</p>
<div className="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 justify-items-center gap-4">
{products.map((product) => (
<Product product={product} key={product.id} />
))}
</div>
</main>
<footer></footer>
</div>
);
}
export async function getStaticProps(context) {
const data = await prisma.product.findMany({
include: {
category: true,
},
});
//convert decimal value to string to pass through as json
const products = data.map((product) => ({
...product,
price: product.price.toString(),
}));
return {
props: { products },
};
}
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+13
View File
@@ -0,0 +1,13 @@
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "tsx prisma/seed.ts",
},
datasource: {
url: env("DATABASE_URL"),
},
});
@@ -1,6 +1,6 @@
const { Prisma } = require("@prisma/client");
import { Prisma } from "../lib/generated/prisma/client";
const categories = [
export const categories = [
{
name: "Hats",
description: "Things you can wear on your head",
@@ -15,38 +15,33 @@ const categories = [
},
];
const products = [
export const products = [
{
name: "Cool helmet.",
description: "A nice helmet to wear on your head",
price: new Prisma.Decimal(19.95),
image: "/images/helmet.jpg",
category_id: 1,
categoryId: 1,
},
{
name: "Grey T-Shirt",
description: "A nice shirt that you can wear on your body",
price: new Prisma.Decimal(22.95),
image: "/images/shirt.jpg",
category_id: 3,
categoryId: 3,
},
{
name: "Socks",
description: "Cool socks that you can wear on your feet",
price: new Prisma.Decimal(12.95),
image: "/images/socks.jpg",
category_id: 2,
categoryId: 2,
},
{
name: "Sweatshirt",
description: "Cool sweatshirt that you can wear on your body",
price: new Prisma.Decimal(12.95),
image: "/images/sweatshirt.jpg",
category_id: 3,
categoryId: 3,
},
];
module.exports = {
products,
categories,
};
+16 -17
View File
@@ -1,29 +1,28 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
provider = "prisma-client"
output = "../lib/generated/prisma"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
referentialIntegrity = "prisma"
provider = "mysql"
relationMode = "prisma"
}
model Product {
id Int @id @default(autoincrement())
name String
description String
price Decimal
image String
category Category? @relation(fields: [category_id], references: [id])
category_id Int
id Int @id @default(autoincrement())
name String
description String
price Decimal
image String
category Category @relation(fields: [categoryId], references: [id])
categoryId Int
@@index([category_id])
@@index([categoryId])
}
model Category {
id Int @id @default(autoincrement())
name String
description String
products Product[]
id Int @id @default(autoincrement())
name String
description String
products Product[]
}
-36
View File
@@ -1,36 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const { categories, products } = require("./data.js");
const prisma = new PrismaClient();
const load = async () => {
try {
await prisma.category.deleteMany();
console.log("Deleted records in category table");
await prisma.product.deleteMany();
console.log("Deleted records in product table");
await prisma.$queryRaw`ALTER TABLE Product AUTO_INCREMENT = 1`;
console.log("reset product auto increment to 1");
await prisma.$queryRaw`ALTER TABLE Category AUTO_INCREMENT = 1`;
console.log("reset category auto increment to 1");
await prisma.category.createMany({
data: categories,
});
console.log("Added category data");
await prisma.product.createMany({
data: products,
});
console.log("Added product data");
} catch (e) {
console.error(e);
process.exit(1);
} finally {
await prisma.$disconnect();
}
};
load();
+49
View File
@@ -0,0 +1,49 @@
import "dotenv/config";
import { PrismaPlanetScale } from "@prisma/adapter-planetscale";
import { PrismaClient } from "../lib/generated/prisma/client";
import { fetch as undiciFetch } from "undici";
import { categories, products } from "./data";
const adapter = new PrismaPlanetScale({
url: process.env.DATABASE_URL,
fetch: undiciFetch,
});
const prisma = new PrismaClient({ adapter });
async function main() {
console.log("Seeding database...");
await prisma.product.deleteMany();
console.log("Deleted records in product table");
await prisma.category.deleteMany();
console.log("Deleted records in category table");
await prisma.$executeRaw`ALTER TABLE Product AUTO_INCREMENT = 1`;
console.log("Reset product auto increment to 1");
await prisma.$executeRaw`ALTER TABLE Category AUTO_INCREMENT = 1`;
console.log("Reset category auto increment to 1");
await prisma.category.createMany({
data: categories,
});
console.log("Added category data");
await prisma.product.createMany({
data: products,
});
console.log("Added product data");
console.log("Seeding complete!");
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
-4
View File
@@ -1,4 +0,0 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-3
View File
@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-11
View File
@@ -1,11 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}