Creating New Features
Complete workflow for building full-stack features in your SaaS application from database to UI
Advanced Developer Content
This guide requires technical knowledge:
- Understanding of Database Schema
- Knowledge of Adding Database Models
- Familiarity with Adding API Routes
- React component basics
Complete workflow for building full-stack features from database to UI. We'll build a "Notes" feature as an example.
Feature Development Workflow
Building a feature involves these layers:
- Database Layer: Define data models and relationships
- API Layer: Create backend endpoints for CRUD operations
- UI Components: Build reusable React components
- Pages: Integrate components into user-facing pages
- Testing & Polish: Validate and refine the feature
Example: Building a Notes Feature
Let's build a complete notes feature where users can create, read, update, and delete personal notes.
Step 1: Database Layer
Define the Note Model
Add the Note model to prisma/schema.prisma:
1model Note {2 id String @id @default(cuid())3 title String4 content String5 color String @default("#FFFFFF")6 isPinned Boolean @default(false)7 userId String8 9 user User @relation(fields: [userId], references: [id], onDelete: Cascade)10 11 createdAt DateTime @default(now())12 updatedAt DateTime @updatedAt13 14 @@index([userId])15 @@index([isPinned])16}Update User Model
Add the relationship to the User model:
1model User {2 id String @id @default(cuid())3 name String?4 email String? @unique5 emailVerified DateTime?6 image String?7 8 accounts Account[]9 sessions Session[]10 todos Todo[]11 notes Note[] // Add this line12 subscription Subscription?13 14 createdAt DateTime @default(now())15 updatedAt DateTime @updatedAt16}Create Migration
1npx prisma migrate dev --name add_notes_featureThis creates the database table and updates the Prisma Client.
Step 2: API Layer
Create CRUD API routes following the pattern from the Adding API Routes guide:
Folder Structure:
src/app/api/notes/
├── route.ts # GET (list), POST (create)
└── [id]/
└── route.ts # GET (one), PUT (update), DELETEEach route should check authentication (await auth()), validate input, and filter by userId. See src/app/api/todos/ for a working reference implementation.
Step 3: UI Components
Create Note Card Component
Create src/components/notes/NoteCard.tsx:
1"use client";2 3import { useState } from "react";4import { Card, CardBody, CardHeader } from "@/components/ui/Card";5import { Button } from "@/components/ui/Button";6import { Trash2, Pin, Edit2 } from "lucide-react";7 8interface Note {9 id: string;10 title: string;11 content: string;12 color: string;13 isPinned: boolean;14 createdAt: Date;15 updatedAt: Date;16}17 18interface NoteCardProps {19 note: Note;20 onEdit: (note: Note) => void;21 onDelete: (id: string) => void;22 onTogglePin: (id: string, isPinned: boolean) => void;23}24 25export function NoteCard({ note, onEdit, onDelete, onTogglePin }: NoteCardProps) {26 const [isDeleting, setIsDeleting] = useState(false);27 28 const handleDelete = async () => {29 if (!confirm("Are you sure you want to delete this note?")) return;30 31 setIsDeleting(true);32 try {33 await onDelete(note.id);34 } catch (error) {35 console.error("Error deleting note:", error);36 setIsDeleting(false);37 }38 };39 40 return (41 <Card42 className="h-full transition-all hover:shadow-lg"43 style={{ backgroundColor: note.color }}44 >45 <CardHeader className="flex flex-row items-start justify-between">46 <h3 className="font-semibold text-lg">{note.title}</h3>47 <Button48 variant="ghost"49 size="sm"50 onClick={() => onTogglePin(note.id, !note.isPinned)}51 className={note.isPinned ? "text-primary-600" : "text-gray-400"}52 >53 <Pin className="w-4 h-4" />54 </Button>55 </CardHeader>56 <CardBody>57 <p className="text-gray-700 dark:text-gray-300 mb-4 whitespace-pre-wrap">58 {note.content}59 </p>60 <div className="flex justify-end gap-2 mt-4">61 <Button62 variant="ghost"63 size="sm"64 onClick={() => onEdit(note)}65 >66 <Edit2 className="w-4 h-4 mr-1" />67 Edit68 </Button>69 <Button70 variant="ghost"71 size="sm"72 onClick={handleDelete}73 disabled={isDeleting}74 className="text-red-600 hover:text-red-700"75 >76 <Trash2 className="w-4 h-4 mr-1" />77 {isDeleting ? "Deleting..." : "Delete"}78 </Button>79 </div>80 </CardBody>81 </Card>82 );83}Create Note Form Component
Create src/components/notes/NoteForm.tsx:
1"use client";2 3import { useState } from "react";4import { Input } from "@/components/ui/Input";5import { Button } from "@/components/ui/Button";6import { Card, CardBody, CardHeader } from "@/components/ui/Card";7 8interface NoteFormProps {9 onSubmit: (data: { title: string; content: string; color: string }) => Promise<void>;10 initialData?: {11 title: string;12 content: string;13 color: string;14 };15 onCancel?: () => void;16}17 18const COLORS = [19 "#FFFFFF", // White20 "#FEF3C7", // Yellow21 "#DBEAFE", // Blue22 "#D1FAE5", // Green23 "#FCE7F3", // Pink24 "#E0E7FF" // Purple25];26 27export function NoteForm({ onSubmit, initialData, onCancel }: NoteFormProps) {28 const [title, setTitle] = useState(initialData?.title || "");29 const [content, setContent] = useState(initialData?.content || "");30 const [color, setColor] = useState(initialData?.color || "#FFFFFF");31 const [isSubmitting, setIsSubmitting] = useState(false);32 33 const handleSubmit = async (e: React.FormEvent) => {34 e.preventDefault();35 36 if (!title.trim() || !content.trim()) {37 alert("Please fill in all fields");38 return;39 }40 41 setIsSubmitting(true);42 try {43 await onSubmit({ title, content, color });44 if (!initialData) {45 // Reset form after create46 setTitle("");47 setContent("");48 setColor("#FFFFFF");49 }50 } catch (error) {51 console.error("Error submitting note:", error);52 } finally {53 setIsSubmitting(false);54 }55 };56 57 return (58 <Card>59 <CardHeader>60 <h2 className="text-xl font-bold">61 {initialData ? "Edit Note" : "Create New Note"}62 </h2>63 </CardHeader>64 <CardBody>65 <form onSubmit={handleSubmit} className="space-y-4">66 <div>67 <label className="block text-sm font-medium mb-2">Title</label>68 <Input69 value={title}70 onChange={(e) => setTitle(e.target.value)}71 placeholder="Note title"72 required73 />74 </div>75 76 <div>77 <label className="block text-sm font-medium mb-2">Content</label>78 <textarea79 value={content}80 onChange={(e) => setContent(e.target.value)}81 placeholder="Write your note here..."82 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary-500 min-h-[150px]"83 required84 />85 </div>86 87 <div>88 <label className="block text-sm font-medium mb-2">Color</label>89 <div className="flex gap-2">90 {COLORS.map((c) => (91 <button92 key={c}93 type="button"94 onClick={() => setColor(c)}95 className={'w-8 h-8 rounded-full border-2 ' + (96 color === c ? "border-primary-600 scale-110" : "border-gray-300"97 ) + ' transition-all'}98 style={{ backgroundColor: c }}99 />100 ))}101 </div>102 </div>103 104 <div className="flex gap-2 justify-end">105 {onCancel && (106 <Button type="button" variant="outline" onClick={onCancel}>107 Cancel108 </Button>109 )}110 <Button type="submit" disabled={isSubmitting}>111 {isSubmitting112 ? "Saving..."113 : initialData114 ? "Update Note"115 : "Create Note"}116 </Button>117 </div>118 </form>119 </CardBody>120 </Card>121 );122}Step 4: Page Integration
Create Notes Page
Create src/app/app/notes/page.tsx:
1"use client";2 3import { useEffect, useState } from "react";4import { NoteCard } from "@/components/notes/NoteCard";5import { NoteForm } from "@/components/notes/NoteForm";6import { Spinner } from "@/components/ui/Spinner";7 8interface Note {9 id: string;10 title: string;11 content: string;12 color: string;13 isPinned: boolean;14 createdAt: Date;15 updatedAt: Date;16}17 18export default function NotesPage() {19 const [notes, setNotes] = useState<Note[]>([]);20 const [loading, setLoading] = useState(true);21 const [editingNote, setEditingNote] = useState<Note | null>(null);22 23 useEffect(() => {24 fetchNotes();25 }, []);26 27 const fetchNotes = async () => {28 try {29 const res = await fetch("/api/notes");30 const data = await res.json();31 setNotes(data.notes);32 } catch (error) {33 console.error("Error fetching notes:", error);34 } finally {35 setLoading(false);36 }37 };38 39 const handleCreate = async (data: { title: string; content: string; color: string }) => {40 const res = await fetch("/api/notes", {41 method: "POST",42 headers: { "Content-Type": "application/json" },43 body: JSON.stringify(data)44 });45 46 if (res.ok) {47 await fetchNotes();48 }49 };50 51 const handleUpdate = async (data: { title: string; content: string; color: string }) => {52 if (!editingNote) return;53 54 const res = await fetch('/api/notes/' + editingNote.id, {55 method: "PUT",56 headers: { "Content-Type": "application/json" },57 body: JSON.stringify(data)58 });59 60 if (res.ok) {61 await fetchNotes();62 setEditingNote(null);63 }64 };65 66 const handleDelete = async (id: string) => {67 const res = await fetch('/api/notes/' + id, {68 method: "DELETE"69 });70 71 if (res.ok) {72 await fetchNotes();73 }74 };75 76 const handleTogglePin = async (id: string, isPinned: boolean) => {77 const res = await fetch('/api/notes/' + id, {78 method: "PUT",79 headers: { "Content-Type": "application/json" },80 body: JSON.stringify({ isPinned })81 });82 83 if (res.ok) {84 await fetchNotes();85 }86 };87 88 if (loading) {89 return (90 <div className="flex justify-center items-center h-96">91 <Spinner size="lg" />92 </div>93 );94 }95 96 return (97 <div className="max-w-7xl mx-auto p-6">98 <h1 className="text-3xl font-bold mb-8">My Notes</h1>99 100 <div className="mb-8">101 {editingNote ? (102 <NoteForm103 onSubmit={handleUpdate}104 initialData={{105 title: editingNote.title,106 content: editingNote.content,107 color: editingNote.color108 }}109 onCancel={() => setEditingNote(null)}110 />111 ) : (112 <NoteForm onSubmit={handleCreate} />113 )}114 </div>115 116 {notes.length === 0 ? (117 <div className="text-center py-12 text-gray-500">118 <p>No notes yet. Create your first note above!</p>119 </div>120 ) : (121 <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">122 {notes.map((note) => (123 <NoteCard124 key={note.id}125 note={note}126 onEdit={setEditingNote}127 onDelete={handleDelete}128 onTogglePin={handleTogglePin}129 />130 ))}131 </div>132 )}133 </div>134 );135}Add Navigation Link
Add a link to your notes page in the dashboard navigation (e.g., src/components/layout/DashboardNav.tsx):
1import { StickyNote } from "lucide-react";2 3// Add to navigation items4{5 label: "Notes",6 href: "/app/notes",7 icon: <StickyNote className="w-5 h-5" />8}Step 5: Testing & Polish
Functional Testing
- Create a note → Verify it appears in the list
- Edit a note → Verify changes are saved
- Delete a note → Verify it's removed
- Pin/unpin notes → Verify pinned notes appear first
- Test color selection → Verify colors display correctly
Edge Cases
- Empty title or content → Should show validation error
- Very long content → Should display properly with scrolling
- Special characters → Should be saved correctly
- Multiple users → Each user should only see their own notes
UI Polish
- Add loading states for all async operations
- Show success/error toasts for user actions
- Implement optimistic UI updates for better UX
- Test dark mode compatibility
- Verify responsive design on mobile/tablet
Best Practices Checklist
Common Pitfalls to Avoid
Security: Missing Authentication
session?.user in API routes. Never trust client-side checks alone.Data Leakage
userId to prevent users from accessing each other's data.Missing Error Handling
Performance
userId, createdAt).Optional Enhancements
Consider adding these enhancements to make your feature more robust:
- Search: Add search functionality to filter notes by title/content
- Categories/Tags: Allow users to organize notes with tags
- Rich Text: Implement a rich text editor for formatted content
- Sharing: Enable note sharing with other users
- Export: Allow users to export notes as PDF or markdown
- Sorting: Add sort options (date, title, pinned status)
- Pagination: Implement pagination for large note lists
- Keyboard Shortcuts: Add keyboard shortcuts for common actions
Related Guides
You're Ready to Build!
You now have a complete template for building features. For your next feature:
- Start with the database model
- Build the API layer
- Create reusable UI components
- Integrate into user-facing pages
- Test thoroughly and polish