Prisma + StackRender: Design Your Database and Start Building Your Backend
Source: Dev.to
What We’ll Do
- Design a PostgreSQL database visually using StackRender
- Deploy the database instantly
- Pull the schema into Prisma automatically
- Start building and testing backend endpoints
Prerequisites
- Node.js installed
- A PostgreSQL database (local or remote)
- Prisma installed and configured in your Node.js project
Note: If you’re new to Prisma, follow the official setup guide before continuing.
Starting With an Empty Project
We begin with a fresh Node.js project where Prisma is already set up. If you open your schema.prisma file, you’ll notice that it’s completely empty — no models, no relations, nothing defined yet. At the same time, the connected PostgreSQL database is also empty (no tables or constraints). This is the perfect starting point.
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
Using StackRender to Design the Schema
Instead of manually designing the schema, we’ll use StackRender, a free and open‑source database schema generator.
-
Create a new database in StackRender
- Name it
ecommerce_db - Choose PostgreSQL as the database type
- Name it
-
You’ll start with an empty diagram. From here you can either design the schema manually or import an existing database. For this example we’ll let the AI handle the initial design.
-
AI Prompt – In StackRender’s AI prompt feature, enter:
Design a multi‑vendor ecommerce database -
Within seconds StackRender generates a complete database diagram, including tables, fields, and relationships. Everything is fully editable — you can rename tables, adjust columns, and fine‑tune relationships before moving forward.
-
Once the schema looks good:
- Open the Code section in StackRender
- Export the generated SQL script
-
Run the exported SQL script against your PostgreSQL database (e.g., using pgAdmin). After execution, all tables, constraints, and relationships are created automatically. Your database is now live without writing SQL by hand.
Syncing the Live Database Back to Prisma
Now that the database is live, we can pull the schema into Prisma:
npx prisma db pull
Prisma introspects the database and automatically generates your schema.prisma file based on the existing tables and relations. Below is the resulting schema (truncated for brevity).
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model cart_items {
id Int @id @default(autoincrement())
cart_id Int
product_id Int
quantity Int
added_at DateTime? @default(now()) @db.Timestamptz(6)
carts carts @relation(fields: [cart_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
products products @relation(fields: [product_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model carts {
id Int @id @default(autoincrement())
user_id Int @unique
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
cart_items cart_items[]
users users @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
/* ... additional models omitted for brevity ... */
Tip: The generated models include all relations, enums, and default values, giving you a ready‑to‑use Prisma client.
Next Steps
-
Generate the Prisma client
npx prisma generate -
Start building API routes (e.g., using Express, Fastify, or NestJS) and import the generated client from
../src/generated/prisma. -
Write integration tests against the live database to verify your endpoints.
-
Iterate – If you need to adjust the schema, go back to StackRender, modify the diagram, re‑export the SQL, run it, and run
npx prisma db pullagain.
Summary
By combining StackRender’s visual designer with Prisma’s introspection, you can:
- Skip manual SQL writing
- Keep your Prisma models perfectly in sync with the database
- Accelerate the “backend‑first” development cycle
Give this workflow a try on your next project and shave hours off your setup time!
Prisma Schema (cleaned)
model cart_items {
id Int @id @default(autoincrement())
cart_id Int
product_id Int
quantity Int
cart carts @relation(fields: [cart_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
product products @relation(fields: [product_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}
model categories {
id Int @id @default(autoincrement())
name String
description String?
products products[]
}
model order_items {
id Int @id @default(autoincrement())
order_id Int
product_id Int
quantity Int
price Decimal @db.Decimal(10, 2)
orders orders @relation(fields: [order_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
products products @relation(fields: [product_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}
model orders {
id Int @id @default(autoincrement())
user_id Int
total_amount Decimal @db.Decimal(10, 2)
currency orders_currency_enum
status orders_order_status_enum @default(pending)
placed_at DateTime? @default(now()) @db.Timestamptz(6)
shipped_at DateTime? @db.Timestamptz(6)
delivered_at DateTime? @db.Timestamptz(6)
cancelled_at DateTime? @db.Timestamptz(6)
users users @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
order_items order_items[]
shipments shipments?
payments payments?
}
model payments {
id Int @id @default(autoincrement())
order_id Int @unique
amount Decimal @db.Decimal(10, 2)
method String?
status payments_payment_status_enum @default(unpaid)
transaction_id String? @unique
payment_date DateTime? @default(now()) @db.Timestamptz(6)
orders orders @relation(fields: [order_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}
model product_images {
id Int @id @default(autoincrement())
product_id Int
url String
alt_text String?
products products @relation(fields: [product_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}
model products {
id Int @id @default(autoincrement())
vendor_id Int
category_id Int?
name String
description String?
price Decimal @db.Decimal(10, 2)
stock_quantity Int
sku String? @unique
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
cart_items cart_items[]
order_items order_items[]
product_images product_images[]
categories categories? @relation(fields: [category_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
reviews reviews[]
}
model reviews {
id Int @id @default(autoincrement())
product_id Int
user_id Int
rating Int
comment String?
review_date DateTime? @default(now()) @db.Timestamptz(6)
products products @relation(fields: [product_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
users users @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model shipments {
id Int @id @default(autoincrement())
order_id Int @unique
shipment_date DateTime? @db.Timestamptz(6)
tracking_number String? @unique
carrier String?
fulfillment_status shipments_fulfillment_status_enum @default(not_fulfilled)
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
orders orders @relation(fields: [order_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}
model users {
id Int @id @default(autoincrement())
username String @unique
email String @unique
password_hash String
first_name String?
last_name String?
shipping_address String?
billing_address String?
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
carts carts?
orders orders[]
reviews reviews[]
}
model vendor_payouts {
id Int @id @default(autoincrement())
vendor_id Int
payout_date DateTime? @default(now()) @db.Timestamptz(6)
amount Decimal @db.Decimal(10, 2)
transaction_id String? @unique
vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model vendors {
id Int @id @default(autoincrement())
name String
email String @unique
phone_number String?
address String?
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
order_items order_items[]
products products[]
vendor_payouts vendor_payouts[]
}
/* ---------- Enums ---------- */
enum orders_currency_enum {
USD
EUR
GBP
}
enum orders_order_status_enum {
pending
processing
shipped
delivered
cancelled
}
enum payments_payment_status_enum {
unpaid
paid
refunded
}
enum shipments_fulfillment_status_enum {
not_fulfilled
fulfilled
partially_fulfilled
}
Simple Backend Feature (POST /product)
The following example shows how to create a POST endpoint that:
- Receives product data from the client.
- Validates the payload with Zod.
- Persists the product (and its images) using Prisma.
- Returns the newly created product together with its vendor, category, and images.
Route Implementation (TypeScript)
import { Request, Response } from "express";
import { prisma } from "./prismaClient"; // adjust the import to your setup
import { createProductSchema, CreateProductImageInput } from "./schemas";
router.post("/product", async (request: Request, response: Response) => {
try {
// 1️⃣ Validate request body
const result = createProductSchema.safeParse(request.body);
if (!result.success) {
response.status(400).json(result);
return;
}
const { data } = result;
// 2️⃣ Create product + nested images
const product = await prisma.products.create({
data: {
...data,
product_images: {
create: data.product_images.map(
(productImage: CreateProductImageInput) => productImage
),
},
},
include: {
vendors: true,
categories: true,
product_images: true,
},
});
// 3️⃣ Respond with the created entity
response.status(200).json({ success: true, data: product });
} catch (error) {
console.error(error);
response.status(500).json({ error, success: false });
}
});
What the Endpoint Does
| Step | Description |
|---|---|
| Receive | Accepts a JSON payload that describes a product and an array of its images. |
| Validate | Uses Zod (createProductSchema) to ensure the payload conforms to the expected shape. |
| Persist | Calls prisma.products.create with a nested create for product_images. |
| Return | Sends back the full product record, including related vendor, category, and product_images. |
End‑to‑End Verification
- Initial state – The
productstable is empty. - Send a test request (e.g., via Postman or curl) with a valid payload.
- Result – A new row appears in
products, and corresponding rows appear inproduct_images. - Response – The API returns the created product together with its related data.
This confirms that:
- The Prisma schema is correctly mapped to the database.
- The generated Prisma client works as expected.
- The validation layer (Zod) catches malformed input before it reaches the DB.
Why This Workflow Is Valuable
- Visual DB design – Start with a diagram or schema file; no hand‑written SQL needed.
- Automatic model generation – Prisma reads the schema and creates type‑safe models for you.
- Rapid prototyping – Jump from schema to a functional endpoint in minutes.
- Consistency – Types, validation, and database constraints stay in sync.
If you’re building back‑ends with Prisma and PostgreSQL, pairing them with a visual design tool (e.g., StackRender) can shave hours—or even days—off your setup time.
Resources
- StackRender – Visual database design & Prisma schema generation.
- Prisma Docs – https://www.prisma.io/docs
- Zod – https://github.com/colinhacks/zod
Happy coding! 🚀
- Website
- Github