Roadmap › Next.js — App Router, Folder Structure, Rendering and Full Stack with SQL Server › Module: Rendering, Data and Caching
Lessons in this course (8)
5. Connecting Next.js to SQL Server
Intermediate★ Must knowLesson 5 of 8
Database code must run only on the server (Server Components, Server Actions, route handlers) — never in Client Components, or credentials would be exposed.
ts
// lib/db.ts
import "server-only";
import sql from "mssql";
const config: sql.config = {
server: process.env.DB_SERVER!,
database: process.env.DB_NAME!,
user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!,
options: { encrypt: true, trustServerCertificate: process.env.NODE_ENV !== "production" },
pool: { max: 10 },
};
// Reuse one pool across hot reloads in development and across requests in production
const globalForDb = globalThis as unknown as { mssqlPool?: Promise<sql.ConnectionPool> };
export const pool = globalForDb.mssqlPool ?? new sql.ConnectionPool(config).connect();
if (process.env.NODE_ENV !== "production") globalForDb.mssqlPool = pool;
// lib/products.ts
import "server-only";
import { pool } from "./db";
import sql from "mssql";
export type Product = { id: number; name: string; price: number };
export async function listProducts(): Promise<Product[]> {
const r = await (await pool).request().query("SELECT Id AS id, Name AS name, Price AS price FROM dbo.Products ORDER BY Name");
return r.recordset;
}
export async function getProduct(id: number): Promise<Product | undefined> {
const r = await (await pool).request().input("id", sql.Int, id)
.query("SELECT Id AS id, Name AS name, Price AS price FROM dbo.Products WHERE Id = @id");
return r.recordset[0];
}
export async function createProduct(p: { name: string; price: number }) {
const r = await (await pool).request().input("name", sql.NVarChar(200), p.name).input("price", sql.Decimal(18, 2), p.price)
.query("INSERT INTO dbo.Products (Name, Price) OUTPUT INSERTED.Id AS id VALUES (@name, @price)");
return { id: r.recordset[0].id as number, ...p };
}bash
# .env.local (not committed; set the same keys in Vercel/Azure settings)
DB_SERVER=localhost
DB_NAME=Shop
DB_USER=shop_app
DB_PASSWORD=change-me- Route handlers using
mssqlmust run on the Node.js runtime (the default), not the Edge runtime. - With Prisma:
provider = "sqlserver"inschema.prisma, a singlePrismaClientinstance kept onglobalThisin development. - Serverless platforms (Vercel) open connections per function instance — keep pools small and prefer a database in the same region; Azure SQL serverless works well.
- Alternative architecture: Next.js frontend calling an ASP.NET Core Web API that owns SQL Server (common in .NET shops) — then Next.js only does
fetchto the API.