Vibe coding with AI course · Module 14: Pinterest Clone

Database - Prisma ORM

1 min read
In this lesson3

Database integration.

Prisma Setup

npm install prisma @prisma/client
npx prisma init

Schema

// prisma/schema.prisma
model User {
  id    Int     @id @default(autoincrement())
  name  String
  email String  @unique
  posts Post[]
}

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  content  String
  author   User   @relation(fields: [authorId], references: [id])
  authorId Int
}

Usage

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Create
await prisma.user.create({
  data: {
    name: 'Jack',
    email: 'jack@pirates.com'
  }
});

// Read
const users = await prisma.user.findMany({
  include: { posts: true }
});

See you there!

Code for this lesson: App.tsx
'use client';

import { useState } from 'react';

interface Board {
  id: string;
  name: string;
  pinCount: number;
  coverImage?: string;
}

const mockBoards: Board[] = [
  { id: '1', name: 'Travel Ideas', pinCount: 247, coverImage: 'https://images.unsplash.com/photo-1488646953014-85cb44e25828?w=100&h=100&fit=crop' },
  { id: '2', name: 'Recipes', pinCount: 156, coverImage: 'https://images.unsplash.com/photo-1495521821757-a1efb6729352?w=100&h=100&fit=crop' },
  { id: '3', name: 'Home Decor', pinCount: 89, coverImage: 'https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?w=100&h=100&fit=crop' }
];

const mockPin = {
  title: 'Beautiful Sunset',
  description: 'Amazing sunset over the ocean with golden hour lighting',
  imageUrl: 'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=100&h=100&fit=crop'
};

export default function SavePinModal() {
  const [isOpen, setIsOpen] = useState(true);
  const [selectedBoardId, setSelectedBoardId] = useState<string | null>(null);

  if (!isOpen) {
    return (
      <div className="min-h-screen bg-gray-100 flex items-center justify-center">
        <button
          onClick={() => setIsOpen(true)}
          className="px-6 py-3 bg-red-600 text-white rounded-full font-semibold"
        >
          Save Pin
        </button>
      </div>
    );
  }

  return (
    <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
      <div className="bg-white rounded-2xl max-w-md w-full max-h-[80vh] flex flex-col">
        {/* Header */}
        <div className="p-6 border-b border-gray-200">
          <div className="flex items-center justify-between">
            <h2 className="text-xl font-bold">Save to board</h2>
            <button onClick={() => setIsOpen(false)} className="text-gray-500 hover:text-black text-xl">
              ×
            </button>
          </div>
        </div>

        {/* Pin Preview */}
        <div className="p-6 border-b border-gray-200">
          <div className="flex gap-3">
            <img src={mockPin.imageUrl} alt={mockPin.title} className="w-16 h-16 rounded object-cover" />
            <div className="flex-1">
              <h3 className="font-semibold">{mockPin.title}</h3>
              <p className="text-sm text-gray-600 line-clamp-2">{mockPin.description}</p>
            </div>
          </div>
        </div>

        {/* Boards */}
        <div className="flex-1 overflow-y-auto p-6">
          <button className="w-full p-4 border-2 border-dashed border-gray-300 rounded-2xl hover:border-gray-400 mb-4">
            <div className="flex items-center gap-3">
              <div className="w-12 h-12 bg-gray-200 rounded flex items-center justify-center text-2xl">
                +
              </div>
              <span className="font-semibold">Create board</span>
            </div>
          </button>

          {mockBoards.map(board => (
            <button
              key={board.id}
              onClick={() => setSelectedBoardId(board.id)}
              className={`w-full p-4 rounded-2xl hover:bg-gray-100 mb-2 ${
                selectedBoardId === board.id ? 'bg-gray-100 ring-2 ring-blue-500' : ''
              }`}
            >
              <div className="flex items-center gap-3">
                {board.coverImage ? (
                  <img src={board.coverImage} alt={board.name} className="w-12 h-12 rounded object-cover" />
                ) : (
                  <div className="w-12 h-12 bg-gray-200 rounded" />
                )}
                <div className="flex-1 text-left">
                  <div className="font-semibold">{board.name}</div>
                  <div className="text-sm text-gray-600">{board.pinCount} pins</div>
                </div>
                {selectedBoardId === board.id && <div className="text-blue-500 text-xl">√</div>}
              </div>
            </button>
          ))}
        </div>

        {/* Save Button */}
        <div className="p-6 border-t border-gray-200">
          <button
            disabled={!selectedBoardId}
            className="w-full py-3 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700 disabled:bg-gray-300"
          >
            Save
          </button>
        </div>
      </div>
    </div>
  );
}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is Prisma in an application?

  2. 2. How do you fetch related records in findMany (e.g. a user's posts)?

Hands-on tasks in the game

  • Click in order

    Click the elements to build the Prisma install command:

  • Code editor

    Create a Prisma model and queries

  • Vertical ordering

    Order the steps of working with the database via Prisma:

Useful articles