A production-ready backend boilerplate with TypeScript, ESM, PostgreSQL, and authentication — configured and ready to ship.
| Layer | Technology |
|---|---|
| Runtime | Node.js 20+ |
| Language | TypeScript (ESM) |
| Framework | Express |
| ORM | Prisma + @prisma/adapter-pg |
| Auth | BetterAuth |
| Database | PostgreSQL |
| Bundler | tsup |
Create a new directory for your project and navigate into it:
mkdir hello-prisma
cd hello-prismaInitialize a simplified package.json and install the necessary TypeScript dependencies:
npm init -y
npm install typescript tsx @types/node --save-dev
npx tsc --initInstall the core dependencies for the server, database, and utilities:
# Production dependencies
npm install prisma @prisma/client @prisma/adapter-pg dotenv express cors cookie-parser better-auth
# Development dependencies
npm install --save-dev @types/express @types/cors @types/cookie-parser tsupUpdate your tsconfig.json to support ESM (ECMAScript Modules) and stricter type checking:
{
"compilerOptions": {
"outDir": "./dist",
"module": "ESNext",
"target": "ES2023",
"moduleResolution": "bundler",
"types": [],
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"strict": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "generated/prisma"]
}Add the following key to your package.json to enable module support:
{
"type": "module"
}Initialize Prisma with PostgreSQL support. This creates a prisma directory and a .env file.
npx prisma init --db --output ../generated/prismaCreate a standardized Prisma client instance with the PostgreSQL adapter for optimal performance.
File: src/lib/prisma.ts
import "dotenv/config";
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/prisma/client';
const connectionString = `${process.env.DATABASE_URL}`;
const adapter = new PrismaPg({ connectionString });
const prisma = new PrismaClient({ adapter });
export { prisma };Better Auth provides a comprehensive authentication solution. Configuring it correctly is crucial for security and performance.
Add the following to your .env file:
# Database connection string
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
# Better Auth Secret (Generate using `openssl rand -base64 32`)
BETTER_AUTH_SECRET=your_generated_secret_key
# Base URL of your application
BETTER_AUTH_URL=http://localhost:3000Create the authentication instance with professional configuration options.
File: src/lib/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "./prisma";
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true
},
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes
},
},
advanced: {
cookiePrefix: "better-auth",
useSecureCookies: process.env.NODE_ENV === "production",
crossSubDomainCookies: {
enabled: false,
},
disableCSRFCheck: true,
defaultCookieAttributes: {
sameSite: "none",
secure: true,
httpOnly: false
}
}
});-
session.cookieCache: Enables caching for session cookies to improve performance by reducing database lookups for session validation.enabled: Activates the cache.maxAge: Sets the cache duration (e.g., 5 minutes), balancing performance with data freshness.
-
advanced.cookiePrefix: Sets a custom prefix ("better-auth") for all cookies generated by the library, preventing naming collisions with other services. -
advanced.useSecureCookies: Enforces theSecureflag on cookies in production environments, ensuring they are only transmitted over HTTPS. -
advanced.defaultCookieAttributes: Defines global defaults for cookie security.sameSite: "none": Allows cross-site requests (necessary if frontend/backend are on different domains).secure: true: Ensures cookies are only sent over encrypted connections.httpOnly: false: Allows client-side JavaScript to access the cookie (configured here asfalse, but typicallytrueis recommended for security unless specific client-side access is required).
For a cleaner project structure, organize your Prisma schema files.
- Create Schema Directory: Create a folder named
schemainside theprismadirectory. - Move Schema File: Move your
schema.prismafile intoprisma/schema. - Update Configuration: Ensure your
package.jsonor Prisma config points to the new schema location if necessary (e.g.,"prisma": { "schema": "prisma/schema" }).
Use the Better Auth CLI to generate the authentication schema directly from your configuration:
npx @better-auth/cli@latest generate --output ./prisma/schema/auth.prisma --config ./src/lib/auth.tsApply the changes to your database:
npx prisma migrate devSet up the Express application with CORS, Middleware, and the Better Auth handler.
File: src/app.ts
import express, { Application } from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./lib/auth";
const app: Application = express();
// Middleware
app.use(cookieParser());
app.use(express.json());
// CORS Setup
const allowedOrigins = ["http://localhost:3000"].filter(Boolean);
app.use(
cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const isAllowed =
allowedOrigins.includes(origin) ||
/^https:\/\/.*\.vercel\.app$/.test(origin); // Allow Vercel deployments
if (isAllowed) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
}
},
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
exposedHeaders: ["Set-Cookie"],
})
);
// Better Auth API Route
app.all('/api/auth/*', toNodeHandler(auth));
// Health Check Route
app.get("/", (_req, res) => {
res.status(200).json({
success: true,
message: "Server is running successfully",
service: "Backend API",
version: "1.0.0",
environment: process.env.NODE_ENV ?? "development",
uptime: process.uptime(),
timestamp: new Date().toISOString(),
});
});
export default app;Create the server entry point to connect to the database and start the application.
File: src/server.ts
import app from "./app";
import { prisma } from "./lib/prisma";
const PORT = process.env.PORT || 5000;
async function main() {
try {
await prisma.$connect();
console.log("Connected to the database successfully.");
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
} catch (error) {
console.error("An error occurred:", error);
await prisma.$disconnect();
process.exit(1);
}
}
main();Add the following scripts to your package.json to streamline development and deployment:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "npx tsx watch src/server.ts",
"seeding": "npx tsx src/scripts/seedingAdmin.ts",
"migrate": "npx prisma migrate dev",
"generate": "npx prisma generate",
"postinstall": "prisma generate",
"build": "prisma generate && tsup src/server.ts --format esm --platform node --target node20 --outDir api --external pg-native"
}Start your server in development mode:
npm run dev