Building a PostgreSQL Backend with Express, Prisma 7, Neon & TypeScript

If you're building a modern Node.js backend, one of the best stacks you can choose today is Express.js, Prisma 7, PostgreSQL, Neon, and TypeScript.
This combination gives you:
⚡ A fast Express server
🛡️ Type-safe database queries with Prisma
☁️ A serverless PostgreSQL database hosted on Neon
🧩 Excellent TypeScript support
📈 A scalable foundation for production applications
In this guide, we'll build the foundation of an Express backend and understand how every piece fits together.
What is Prisma?
Prisma is an Object Relational Mapper (ORM).
Instead of writing raw SQL queries, Prisma lets you interact with your database using JavaScript or TypeScript.
For example, instead of writing:
SELECT * FROM users;
you can simply write:
const users = await prisma.user.findMany();
Prisma translates that into SQL behind the scenes, while giving you autocompletion, type safety, and excellent developer experience.
What is Neon?
Neon is a serverless PostgreSQL platform.
Think of it as the PostgreSQL equivalent of MongoDB Atlas.
MongoDB Atlas → MongoDB
Neon → PostgreSQL
Instead of managing your own PostgreSQL server, Neon hosts and manages everything for you, making it an excellent choice for modern applications.
Step 1 — Install Dependencies
First, install the packages required by Express.
npm install express dotenv bcryptjs jsonwebtoken cors cookie-parser morgan
These packages provide the core functionality of our backend.
PackagePurposeexpressBackend frameworkdotenvLoads environment variablesbcryptjsHashes passwordsjsonwebtokenCreates JWT authentication tokenscorsAllows frontend applications to communicate with the APIcookie-parserReads cookies from incoming requestsmorganLogs incoming HTTP requests
Next, install Prisma and PostgreSQL packages.
npm install @prisma/client @prisma/adapter-pg pg
PackagePurpose@prisma/clientGenerated database client@prisma/adapter-pgPostgreSQL adapter for Prisma 7pgPostgreSQL driver
Finally, install the development dependencies.
npm install -D prisma typescript tsx @types/node @types/express @types/jsonwebtoken @types/cors @types/cookie-parser @types/morgan
These packages provide the TypeScript compiler, Prisma CLI, and type definitions.
Step 2 — Initialize TypeScript
Create a TypeScript configuration file.
npx tsc --init
This generates:
tsconfig.json
This file tells TypeScript how to compile your project.
Step 3 — Initialize Prisma
Now initialize Prisma.
npx prisma init --datasource-provider postgresql --output ../src/generated/prisma
This creates:
prisma/
schema.prisma
prisma.config.ts
.env
One important change in Prisma 7 is that the database connection string is no longer stored in schema.prisma.
Instead, it lives inside prisma.config.ts.
Step 4 — Configure Environment Variables
Create a .env file.
PORT=3000
DATABASE_URL="your_neon_database_url?sslmode=verify-full"
JWT_SECRET="your_secret_key"
Each variable has a specific purpose:
PORT — tells Express which port to run on.
DATABASE_URL — connects Prisma to your Neon PostgreSQL database.
JWT_SECRET — signs and verifies authentication tokens.
Step 5 — Configure Prisma
Create prisma.config.ts.
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});
Let's understand what this configuration does.
First, we load environment variables.
import "dotenv/config";
Next, we tell Prisma where our schema file lives.
schema: "prisma/schema.prisma"
Then we specify where migrations should be stored.
migrations: {
path: "prisma/migrations",
}
Finally, Prisma reads the database URL from our environment variables.
datasource: {
url: env("DATABASE_URL"),
}
Step 6 — Define Your Database Models
Inside prisma/schema.prisma, create your models.
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id String @id @default(uuid())
name String
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Let's break this down.
The generator creates the Prisma Client that your application will use.
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
The datasource tells Prisma that we're using PostgreSQL.
datasource db {
provider = "postgresql"
}
Notice that the database URL is not defined here anymore.
Instead, Prisma reads it from prisma.config.ts.
Our User model defines the structure of the users table.
model User {
id String @id @default(uuid())
name String
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Some important fields include:
@default(uuid())automatically generates unique IDs.@uniqueprevents duplicate email addresses.@updatedAtautomatically updates whenever the record changes.
Always store hashed passwords, never plain-text passwords.
Step 7 — Create Database Tables
Once your schema is ready, create the database tables.
npx prisma migrate dev --name init
Here's what happens behind the scenes:
schema.prisma
↓
Migration Generated
↓
SQL Generated
↓
PostgreSQL Tables Created
Step 8 — Generate Prisma Client
Now generate the Prisma Client.
npx prisma generate
This creates a fully typed database client that your application will use.
Step 9 — Create a Shared Prisma Instance
Create src/configs/prisma.ts.
import "dotenv/config";
import { PrismaClient } from "../../generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});
const prisma = new PrismaClient({
adapter,
});
export default prisma;
Instead of creating a new Prisma client inside every controller, we create it once and reuse it throughout the application.
This improves performance and keeps the code clean.
Prisma 7 vs Older Versions
One of the biggest differences in Prisma 7 is how the client is initialized.
Older versions looked like this:
const prisma = new PrismaClient();
Prisma 7 requires the PostgreSQL adapter.
const prisma = new PrismaClient({
adapter,
});
If you forget the adapter, Prisma will fail to connect to PostgreSQL.
Step 10 — Create the Express Application
Create src/app.ts.
import express from "express";
import authRouter from "./routes/authRoutes";
const app = express();
app.use(express.json());
app.use("/api/v1/auth", authRouter);
app.get("/", (req, res) => {
res.send("API is running");
});
export default app;
This file creates the Express application, registers middleware, and mounts your API routes.
Step 11 — Start the Server
Create src/index.ts.
import "dotenv/config";
import app from "./app";
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
This file starts the Express server and listens for incoming requests.
Step 12 — Create Authentication Routes
import express from "express";
import { login, register } from "../controllers/authController";
const authRouter = express.Router();
authRouter.post("/register", register);
authRouter.post("/login", login);
export default authRouter;
These routes expose two authentication endpoints:
POST /api/v1/auth/register
POST /api/v1/auth/login
TypeScript Tip
When importing Express types, use:
import type { Request, Response } from "express";
instead of
import { Request, Response } from "express";
The type keyword tells TypeScript that these imports are only needed during compilation and should be removed from the final JavaScript bundle.
How Registration Works
A typical registration request follows this flow:
User
↓
Send name, email and password
↓
Validate input
↓
Check if email already exists
↓
Hash password
↓
Create user
↓
Return response
How Login Works
The login process is similar:
User
↓
Send email and password
↓
Find user by email
↓
Compare password hash
↓
Generate JWT
↓
Return authentication token
Common Prisma 7 Errors
The datasource property "url" is no longer supported
If you see:
The datasource property "url" is no longer supported in schema files
remove
url = env("DATABASE_URL")
from schema.prisma.
The database URL now belongs in prisma.config.ts.
Named export 'PrismaClient' not found
This usually means you're mixing an older Prisma setup with the new Prisma 7 configuration.
Double-check your generator and client output path.
PrismaClient needs to be constructed with a valid PrismaClientOptions
This typically means the PostgreSQL adapter is missing.
Always initialize Prisma like this:
const prisma = new PrismaClient({
adapter,
});
The Prisma Mental Model
Understanding how everything connects makes Prisma much easier to learn.
.env
↓
DATABASE_URL
↓
prisma.config.ts
↓
schema.prisma
↓
Migration
↓
PostgreSQL Tables
↓
Prisma Client
↓
Controllers
↓
CRUD Operations
Once you understand this flow, Prisma becomes much less intimidating. Your schema defines the shape of your data, migrations turn that schema into real database tables, Prisma Client gives you a type-safe way to query those tables, and your Express controllers use that client to implement your application's business logic.
With this foundation in place, you're ready to build authentication, REST APIs, and full-stack applications on top of a robust PostgreSQL database.