Skip to Content

Basic CRUD Example

This example demonstrates a complete implementation of a CRUD application using CrudKit, Drizzle ORM, tRPC, and React Query.

Project Structure

├── src/ │ ├── db/ │ │ ├── index.ts # Database connection │ │ └── schema.ts # Drizzle schema definition │ ├── server/ │ │ ├── api/ │ │ │ ├── routers/ │ │ │ │ └── tasks.ts # Task router using CrudKit │ │ │ ├── root.ts # Root tRPC router │ │ │ └── trpc.ts # tRPC configuration │ ├── hooks/ │ │ └── use-tasks.ts # React Query hooks using CrudKit │ └── components/ │ ├── TaskList.tsx # List of tasks with CRUD operations │ └── TaskForm.tsx # Form for creating/editing tasks

Database Schema

First, let’s define our database schema using Drizzle:

// src/db/schema.ts import { pgTable, serial, text, timestamp, boolean } from 'drizzle-orm/pg-core'; export const tasks = pgTable('tasks', { id: serial('id').primaryKey(), title: text('title').notNull(), description: text('description'), completed: boolean('completed').default(false).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at'), });

Database Connection

// src/db/index.ts import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); export const db = drizzle(pool);

tRPC Setup

// src/server/api/trpc.ts import { initTRPC, TRPCError } from '@trpc/server'; import { type CreateNextContextOptions } from '@trpc/server/adapters/next'; import { db } from '../../db'; export const createTRPCContext = (opts: CreateNextContextOptions) => { const { req, res } = opts; // Simple auth check on the server const user = getUserFromRequest(req); return { db, user, req, res, }; }; const t = initTRPC.context<typeof createTRPCContext>().create(); export const createTRPCRouter = t.router; export const publicProcedure = t.procedure; // Protected procedure for authenticated routes export const protectedProcedure = t.procedure.use(({ ctx, next }) => { if (!ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { ...ctx, user: ctx.user, }, }); }); // Helper function to get user from request function getUserFromRequest(req) { // This is a simplified example - implement your own auth logic const authHeader = req.headers.authorization; if (!authHeader) return null; // Extract and verify token try { // Return user if authenticated return { id: '1', name: 'Test User' }; } catch { return null; } }

CrudKit Router Setup

// src/server/api/routers/tasks.ts import { createTRPCRouter, publicProcedure, protectedProcedure } from '../trpc'; import { generateRouter } from 'crudkit'; import { tasks } from '../../../db/schema'; export const tasksRouter = generateRouter( { table: tasks, customProcedures: { // Custom query example: get completed tasks count getCompletedCount: publicProcedure.query(async ({ ctx }) => { const result = await ctx.db .select({ count: sql`count(*)` }) .from(tasks) .where(eq(tasks.completed, true)); return Number(result[0].count); }), // Custom mutation example: mark all tasks as complete markAllComplete: protectedProcedure.mutation(async ({ ctx }) => { return await ctx.db .update(tasks) .set({ completed: true, updatedAt: new Date() }) .returning(); }), }, }, { createTRPCRouter, publicProcedure, protectedProcedure, } );

Root Router

// src/server/api/root.ts import { createTRPCRouter } from './trpc'; import { tasksRouter } from './routers/tasks'; export const appRouter = createTRPCRouter({ tasks: tasksRouter, }); export type AppRouter = typeof appRouter;

React Query Hooks

// src/hooks/use-tasks.ts import { generateReactQueryHooks } from 'crudkit'; import { trpc } from '../utils/trpc'; import { tasks } from '../db/schema'; export const useTasks = generateReactQueryHooks( { routerPath: 'tasks', }, trpc );

Components

Task List Component

// src/components/TaskList.tsx import { useState } from 'react'; import { useTasks } from '../hooks/use-tasks'; import { TaskForm } from './TaskForm'; export function TaskList() { const [editingTask, setEditingTask] = useState(null); const { data: tasks, isLoading } = useTasks.useGetAll(); const { delete: deleteTask } = useTasks.useDelete(); const { update } = useTasks.useUpdate(); const utils = trpc.useContext(); // Toggle task completion status const handleToggleComplete = (task) => { update({ id: task.id, completed: !task.completed, }, { onSuccess: () => { utils.tasks.getAll.invalidate(); } }); }; // Delete a task const handleDelete = (id) => { if (window.confirm('Are you sure you want to delete this task?')) { deleteTask({ id }, { onSuccess: () => { utils.tasks.getAll.invalidate(); } }); } }; if (isLoading) return <div>Loading tasks...</div>; return ( <div> <h2>Tasks</h2> {editingTask ? ( <div> <h3>Edit Task</h3> <TaskForm task={editingTask} onCancel={() => setEditingTask(null)} onSuccess={() => { setEditingTask(null); utils.tasks.getAll.invalidate(); }} /> </div> ) : ( <TaskForm onSuccess={() => utils.tasks.getAll.invalidate()} /> )} <ul className="task-list"> {tasks?.map((task) => ( <li key={task.id} className={task.completed ? 'completed' : ''}> <div className="task-header"> <input type="checkbox" checked={task.completed} onChange={() => handleToggleComplete(task)} /> <h3>{task.title}</h3> <div className="actions"> <button onClick={() => setEditingTask(task)}>Edit</button> <button onClick={() => handleDelete(task.id)}>Delete</button> </div> </div> {task.description && ( <p className="task-description">{task.description}</p> )} </li> ))} </ul> </div> ); }

Task Form Component

// src/components/TaskForm.tsx import { useState } from 'react'; import { useTasks } from '../hooks/use-tasks'; export function TaskForm({ task, onSuccess, onCancel }) { const [title, setTitle] = useState(task?.title || ''); const [description, setDescription] = useState(task?.description || ''); const { create, isLoading: isCreating } = useTasks.useCreate(); const { update, isLoading: isUpdating } = useTasks.useUpdate(); const isLoading = isCreating || isUpdating; const isEditing = !!task; const handleSubmit = (e) => { e.preventDefault(); if (isEditing) { update({ id: task.id, title, description, updatedAt: new Date(), }, { onSuccess, }); } else { create({ title, description, }, { onSuccess: () => { setTitle(''); setDescription(''); if (onSuccess) onSuccess(); }, }); } }; return ( <form onSubmit={handleSubmit} className="task-form"> <div className="form-row"> <label htmlFor="title">Title</label> <input id="title" value={title} onChange={(e) => setTitle(e.target.value)} required disabled={isLoading} /> </div> <div className="form-row"> <label htmlFor="description">Description</label> <textarea id="description" value={description} onChange={(e) => setDescription(e.target.value)} disabled={isLoading} rows={3} /> </div> <div className="form-actions"> {isEditing && ( <button type="button" onClick={onCancel} disabled={isLoading}> Cancel </button> )} <button type="submit" disabled={isLoading}> {isLoading ? isEditing ? 'Updating...' : 'Creating...' : isEditing ? 'Update Task' : 'Create Task' } </button> </div> </form> ); }

Complete Page Component

// src/pages/tasks.tsx import { TaskList } from '../components/TaskList'; import { trpc } from '../utils/trpc'; export default function TasksPage() { const { data: completedCount } = trpc.tasks.getCompletedCount.useQuery(); const { mutate: markAllComplete, isLoading } = trpc.tasks.markAllComplete.useMutation({ onSuccess: () => { utils.tasks.getAll.invalidate(); utils.tasks.getCompletedCount.invalidate(); } }); const utils = trpc.useContext(); return ( <div className="container"> <h1>Task Manager</h1> <div className="stats"> <p>Completed tasks: {completedCount || 0}</p> <button onClick={() => markAllComplete()} disabled={isLoading} > {isLoading ? 'Updating...' : 'Mark All Complete'} </button> </div> <TaskList /> </div> ); }

CSS Styling

To complete the example, you could add some simple CSS styles:

/* src/styles/tasks.css */ .task-list { list-style: none; padding: 0; } .task-list li { border: 1px solid #ddd; border-radius: 4px; padding: 1rem; margin-bottom: 1rem; } .task-list li.completed h3 { text-decoration: line-through; color: #888; } .task-header { display: flex; align-items: center; } .task-header h3 { margin: 0 0 0 0.5rem; flex: 1; } .actions { display: flex; gap: 0.5rem; } .task-description { margin-top: 0.5rem; margin-left: 1.5rem; color: #666; } .task-form { margin-bottom: 2rem; padding: 1rem; border: 1px solid #eee; border-radius: 4px; } .form-row { margin-bottom: 1rem; } .form-row label { display: block; margin-bottom: 0.25rem; font-weight: 500; } .form-row input, .form-row textarea { width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; } .form-actions { display: flex; justify-content: flex-end; gap: 0.5rem; } .stats { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding: 1rem; background-color: #f5f5f5; border-radius: 4px; }

This example demonstrates a complete CRUD application using CrudKit. You can adapt it to your specific requirements and expand upon it as needed.

Last updated on