ساخت برنامه Todo با Drizzle و PostgreSQL , MySQL و SQLite در NextJS


برای ساخت یک برنامه Todo با استفاده از فریم‌ورک NextJS و Drizzle ORM، در ابتدا، بایستی با اجرای دستور زیر، برنامه NextJS خود را ایجاد کنید:

کپی
npx create-next-app@latest drizzle-todo-app

در ادامه، وارد دایرکتوری پروژه شوید و متناسب با دیتابیس انتخابی خود، مراحل ساخت برنامه را جلو ببرید.

با اجرای دستورات زیر، وابستگی‌های برنامه را نصب کنید:

کپی
npm update --save
npm install drizzle-orm dotenv pg
npm install -D drizzle-kit

در ادامه، برای به خطا نخوردن برنامه، قطعه کد زیر را بهcompilerOptions در فایلtsconfig.json اضافه کنید:

کپی
"target": "es2017",

در مسیر اصلی پروژه، یک فایل به نام env.ایجاد کنید و URI مربوط به دیتابیس خود را در آنجا در متغیرDATABASE_URL قرار دهید، به عنوان مثال:

کپی
DATABASE_URL=postgresql://root:XkYgSzHmMAf9chdgp2OXOtlb@bromo.liara.cloud:32308/test_db

در دایرکتوری src یک فایل به نام db.ts ایجاد کنید و قطعه کد زیر را در آن، قرار دهید:

کپی
import { drizzle } from "drizzle-orm/node-postgres";
import { Client } from "pg";
import { config } from 'dotenv';

config({ path: '.env' });

const client = new Client({
    connectionString: process.env.DATABASE_URL,
  });


await client.connect();
export const db = drizzle(client);

مجدداً در دایرکتوری src، یک فایل دیگر به نامschema.ts قرار دهید و قطعه کد زیر را در آن، وارد کنید:

کپی
import { boolean, integer, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const todoTable = pgTable('todo_table', {
  id: serial('id').primaryKey(),
  text: text('text').notNull(),
  done: boolean('done').default(false),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

export type TodoType = typeof todoTable.$inferSelect;

سپس، در مسیر اصلی پروژه، یک فایل به نامdrizzle.config.ts ایجاد کنید و قطعه کد زیر را در آن، قرار دهید:

کپی
import { config } from 'dotenv';
import { defineConfig } from 'drizzle-kit';

config({ path: '.env' });

export default defineConfig({
  schema: './src/schema.ts',
  out: './migrations',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

در نظر داشته باشید که کار با Drizzle در سه مرحله می‌تواند خلاصه شود:

۱

تعریف Schema

۲

ایجاد فایل‌های migration از schema

۳

اجرای migrationها در دیتابیس

اکنون، می‌توانید با اجرای دو دستور زیر در ترمینال پروژه اصلی خود، مرحله دوم و سوم را نیز انجام دهید:

کپی
npx drizzle-kit generate
npx drizzle-kit migrate

سپس، بایستی در مسیر src/pages/api یک فایل به نام todos.ts ایجاد کنید و قطعه کد زیر را، در آن، قرار دهید:

کپی
import type { NextApiRequest, NextApiResponse } from 'next';
import { db } from '@/db';
import { todoTable, TodoType } from '@/schema';
import { eq } from 'drizzle-orm';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { method } = req;

  switch (method) {
    case 'POST':
      const { text } = req.body;
      await db.insert(todoTable).values({ text });
      res.status(201).end();
      break;

    case 'GET':
      const todos: TodoType[] = await db.select().from(todoTable);
      res.status(200).json(todos);
      break;

    case 'PUT':
      const { id, text: newText, done } = req.body;
      await db.update(todoTable).set({ text: newText, done: done ? true : false }).where(eq(todoTable.id, id));
      res.status(200).end();
      break;

    case 'DELETE':
      const { id: deleteId } = req.body;
      await db.delete(todoTable).where(eq(todoTable.id, deleteId));
      res.status(200).end();
      break;

    default:
      res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE']);
      res.status(405).end(`Method ${method} Not Allowed`);
  }
}

اکنون می‌توانید componentهای مربوط به front-end را نیز ایجاد کنید. برای این‌کار می‌توانید در دایرکتوری src، یک دایرکتوری به نام components ایجاد کنید و درون این دایرکتوری، یک فایل به نام AddTodo.tsx ایجاد کنید و قطعه کد زیر را، در آن، قرار دهید:

کپی
import { useState, FormEvent } from 'react';

interface AddTodoProps {
  onAdd: () => void;
}

const AddTodo = ({ onAdd }: AddTodoProps) => {
  const [text, setText] = useState<string>('');

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (!text.trim()) return;

    const res = await fetch('/api/todos', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ text }),
    });

    if (res.ok) {
      onAdd();
      setText('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={text} 
        onChange={(e) => setText(e.target.value)} 
        placeholder="Add a new task" 
      />
      <button type="submit">Add</button>
    </form>
  );
};

export default AddTodo;

همچنین، در همین مسیر، بایستی یک فایل به نام Todo.tsx ایجاد کرده و قطعه کد زیر را، در آن، قرار دهید:

کپی
import { useState } from 'react';
import { TodoType } from '@/schema';

interface TodoProps {
  todo: TodoType;
  onUpdate: () => void;
  onDelete: () => void;
}

const Todo = ({ todo, onUpdate, onDelete }: TodoProps) => {
  const [isEditing, setIsEditing] = useState(false);
  const [text, setText] = useState(todo.text);

  const handleEdit = async () => {
    await fetch('/api/todos', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ id: todo.id, text, done: todo.done }),
    });
    setIsEditing(false);
    onUpdate();
  };

  const handleToggle = async () => {
    await fetch('/api/todos', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ id: todo.id, text: todo.text, done: !todo.done }),
    });
    onUpdate();
  };

  const handleDelete = async () => {
    await fetch('/api/todos', {
      method: 'DELETE',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ id: todo.id }),
    });
    onDelete();
  };

  return (
    <div className="todo-item">
      {isEditing ? (
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
      ) : (
        <span className={`todo-text ${todo.done ? 'done' : ''}`}>
          {todo.text}
        </span>
      )}
      <div className="todo-actions">
        {isEditing ? (
          <button onClick={handleEdit}>Save</button>
        ) : (
          <>
            <button onClick={() => setIsEditing(true)}>Edit</button>
            <button onClick={handleToggle}>
              {todo.done ? 'Undone' : 'Done'}
            </button>
            <button onClick={handleDelete}>Delete</button>
          </>
        )}
      </div>
    </div>
  );
};

export default Todo;

در نهایت، بایستی یک component دیگر به نام Todos.tsx ایجاد کنید و قطعه کد زیر را، در آن قرار دهید:

کپی
// src/components/Todos.tsx
import { useState, useEffect } from 'react';
import Todo from './Todo';
import AddTodo from './AddTodo';
import { TodoType } from '@/schema';

const Todos = () => {
  const [todos, setTodos] = useState<TodoType[]>([]);

  const fetchTodos = async () => {
    const res = await fetch('/api/todos');
    const data: TodoType[] = await res.json();
    setTodos(data);
  };

  useEffect(() => {
    fetchTodos();
  }, []);

  const activeTodos = todos.filter(todo => !todo.done);
  const doneTodos = todos.filter(todo => todo.done);

  return (
    <div>
      <AddTodo onAdd={fetchTodos} />
      <h2>Active Todos</h2>
      <div>
        {activeTodos.map((todo) => (
          <Todo key={todo.id} todo={todo} onUpdate={fetchTodos} onDelete={fetchTodos} />
        ))}
      </div>
      <h2>Done Todos</h2>
      <div>
        {doneTodos.map((todo) => (
          <Todo key={todo.id} todo={todo} onUpdate={fetchTodos} onDelete={fetchTodos} />
        ))}
      </div>
    </div>
  );
};

export default Todos;

در انتها، بایستی در فایل src/pages/index.tsx قطعه کد زیر را، قرار دهید:

کپی
import Todos from '@/components/Todos';

export default function Home() {
  return (
    <div>
      <h1>Todo List</h1>
      <Todos />
    </div>
  );
}

تمامی کارها انجام شده است و می‌توانید برنامه خود را با دستور زیر، اجرا کرده و از آن، استفاده کنید:

کپی
npm run dev

یک نمونه کامل از پروژه‌ فوق که آماده مستقر شدن در لیارا است را می‌توانید در اینجا مشاهده کنید.