Tutorial: Build Your First Feature

Learn how to build a complete feature using Bootspring's agents, skills, and workflows.

What You'll Build#

A user profile page with:

  • Profile display with avatar
  • Edit functionality
  • Form validation
  • Database persistence

Prerequisites#

  • Bootspring initialized in your project
  • Next.js with TypeScript
  • Prisma configured with a database

Time Required#

Approximately 30 minutes.

Step 1: Start the Feature Development Workflow#

Let's use Bootspring's workflow to structure our development.

bootspring workflow start feature-development

When prompted:

  • Feature name: User Profile Page
  • Requirements: Display user info, edit profile, avatar upload

The workflow will guide us through: Plan → Design → Build → Test → Review

Step 2: Planning Phase#

The workflow starts with the architecture-expert agent.

Ask:

Plan the user profile feature with display and edit capabilities. Consider the existing user model and Clerk authentication.

The agent will provide:

  • Component structure
  • Data requirements
  • API endpoints needed
  • Suggested approach

Example output:

1## User Profile Feature Plan 2 3### Components 41. ProfilePage - Main page component 52. ProfileHeader - Avatar and name display 63. ProfileForm - Edit form with validation 74. AvatarUpload - Image upload component 8 9### API Endpoints 10- GET /api/user/profile - Fetch profile 11- PATCH /api/user/profile - Update profile 12 13### Database 14- Extend User model with profile fields 15 16### Approach 171. Add profile fields to User model 182. Create API endpoints 193. Build UI components 204. Add form validation 215. Implement avatar upload

Checkpoint: Review the plan and confirm it matches your needs.

Step 3: Design Phase#

The workflow moves to the design phase with database-expert and api-expert.

Database Schema#

Ask the database-expert:

Design the profile fields for the User model. Include: bio, website, location, avatar URL, social links.

Update your Prisma schema:

1// prisma/schema.prisma 2model User { 3 id String @id @default(cuid()) 4 clerkId String @unique 5 email String @unique 6 name String? 7 8 // Profile fields 9 bio String? 10 website String? 11 location String? 12 avatarUrl String? 13 twitter String? 14 github String? 15 linkedin String? 16 17 createdAt DateTime @default(now()) 18 updatedAt DateTime @updatedAt 19}

Run the migration:

npx prisma db push

API Design#

Ask the api-expert:

Design the profile API endpoints with proper validation.

Output:

1// API Contract 2 3// GET /api/user/profile 4// Response: { profile: UserProfile } 5 6// PATCH /api/user/profile 7// Body: { bio?, website?, location?, twitter?, github?, linkedin? } 8// Response: { profile: UserProfile } 9 10// Validation 11const profileSchema = z.object({ 12 bio: z.string().max(500).optional(), 13 website: z.string().url().optional(), 14 location: z.string().max(100).optional(), 15 twitter: z.string().max(50).optional(), 16 github: z.string().max(50).optional(), 17 linkedin: z.string().max(100).optional(), 18});

Checkpoint: Schema and API design complete.

Step 4: Build Phase#

The workflow enters the build phase with backend-expert and frontend-expert.

Apply the API Skill#

bootspring skill apply api/rest-crud

Create the API Route#

Ask the backend-expert:

Create the profile API route with Prisma and Clerk auth.

Create the file:

1// app/api/user/profile/route.ts 2import { auth, currentUser } from '@clerk/nextjs'; 3import { prisma } from '@/lib/prisma'; 4import { NextResponse } from 'next/server'; 5import { z } from 'zod'; 6 7const profileSchema = z.object({ 8 bio: z.string().max(500).optional(), 9 website: z.string().url().optional().or(z.literal('')), 10 location: z.string().max(100).optional(), 11 twitter: z.string().max(50).optional(), 12 github: z.string().max(50).optional(), 13 linkedin: z.string().max(100).optional(), 14}); 15 16export async function GET() { 17 const { userId } = auth(); 18 19 if (!userId) { 20 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 21 } 22 23 const user = await prisma.user.findUnique({ 24 where: { clerkId: userId }, 25 select: { 26 id: true, 27 name: true, 28 email: true, 29 bio: true, 30 website: true, 31 location: true, 32 avatarUrl: true, 33 twitter: true, 34 github: true, 35 linkedin: true, 36 }, 37 }); 38 39 if (!user) { 40 return NextResponse.json({ error: 'User not found' }, { status: 404 }); 41 } 42 43 return NextResponse.json({ profile: user }); 44} 45 46export async function PATCH(request: Request) { 47 const { userId } = auth(); 48 49 if (!userId) { 50 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 51 } 52 53 const body = await request.json(); 54 const result = profileSchema.safeParse(body); 55 56 if (!result.success) { 57 return NextResponse.json( 58 { error: 'Invalid data', details: result.error.issues }, 59 { status: 400 } 60 ); 61 } 62 63 const user = await prisma.user.update({ 64 where: { clerkId: userId }, 65 data: result.data, 66 select: { 67 id: true, 68 name: true, 69 email: true, 70 bio: true, 71 website: true, 72 location: true, 73 avatarUrl: true, 74 twitter: true, 75 github: true, 76 linkedin: true, 77 }, 78 }); 79 80 return NextResponse.json({ profile: user }); 81}

Create the Profile Page#

Ask the frontend-expert:

Create a profile page with display and edit modes. Use React Hook Form with Zod validation.

Create the page:

1// app/(dashboard)/profile/page.tsx 2import { auth } from '@clerk/nextjs'; 3import { redirect } from 'next/navigation'; 4import { prisma } from '@/lib/prisma'; 5import { ProfileForm } from '@/components/profile/ProfileForm'; 6 7export default async function ProfilePage() { 8 const { userId } = auth(); 9 10 if (!userId) { 11 redirect('/sign-in'); 12 } 13 14 const user = await prisma.user.findUnique({ 15 where: { clerkId: userId }, 16 }); 17 18 if (!user) { 19 redirect('/onboarding'); 20 } 21 22 return ( 23 <div className="container max-w-2xl py-8"> 24 <h1 className="text-2xl font-bold mb-6">Your Profile</h1> 25 <ProfileForm user={user} /> 26 </div> 27 ); 28}

Create the form component:

1// components/profile/ProfileForm.tsx 2'use client'; 3 4import { useForm } from 'react-hook-form'; 5import { zodResolver } from '@hookform/resolvers/zod'; 6import { z } from 'zod'; 7import { useState } from 'react'; 8import { useRouter } from 'next/navigation'; 9 10const profileSchema = z.object({ 11 bio: z.string().max(500).optional(), 12 website: z.string().url().optional().or(z.literal('')), 13 location: z.string().max(100).optional(), 14 twitter: z.string().max(50).optional(), 15 github: z.string().max(50).optional(), 16 linkedin: z.string().max(100).optional(), 17}); 18 19type ProfileFormData = z.infer<typeof profileSchema>; 20 21interface ProfileFormProps { 22 user: { 23 bio: string | null; 24 website: string | null; 25 location: string | null; 26 twitter: string | null; 27 github: string | null; 28 linkedin: string | null; 29 }; 30} 31 32export function ProfileForm({ user }: ProfileFormProps) { 33 const router = useRouter(); 34 const [isLoading, setIsLoading] = useState(false); 35 const [error, setError] = useState<string | null>(null); 36 37 const { 38 register, 39 handleSubmit, 40 formState: { errors, isDirty }, 41 } = useForm<ProfileFormData>({ 42 resolver: zodResolver(profileSchema), 43 defaultValues: { 44 bio: user.bio || '', 45 website: user.website || '', 46 location: user.location || '', 47 twitter: user.twitter || '', 48 github: user.github || '', 49 linkedin: user.linkedin || '', 50 }, 51 }); 52 53 const onSubmit = async (data: ProfileFormData) => { 54 setIsLoading(true); 55 setError(null); 56 57 try { 58 const response = await fetch('/api/user/profile', { 59 method: 'PATCH', 60 headers: { 'Content-Type': 'application/json' }, 61 body: JSON.stringify(data), 62 }); 63 64 if (!response.ok) { 65 throw new Error('Failed to update profile'); 66 } 67 68 router.refresh(); 69 } catch (err) { 70 setError('Something went wrong. Please try again.'); 71 } finally { 72 setIsLoading(false); 73 } 74 }; 75 76 return ( 77 <form onSubmit={handleSubmit(onSubmit)} className="space-y-6"> 78 {error && ( 79 <div className="p-4 bg-red-50 text-red-600 rounded-lg"> 80 {error} 81 </div> 82 )} 83 84 <div> 85 <label className="block text-sm font-medium mb-2">Bio</label> 86 <textarea 87 {...register('bio')} 88 rows={4} 89 className="w-full px-3 py-2 border rounded-lg" 90 placeholder="Tell us about yourself..." 91 /> 92 {errors.bio && ( 93 <p className="text-red-500 text-sm mt-1">{errors.bio.message}</p> 94 )} 95 </div> 96 97 <div> 98 <label className="block text-sm font-medium mb-2">Website</label> 99 <input 100 {...register('website')} 101 type="url" 102 className="w-full px-3 py-2 border rounded-lg" 103 placeholder="https://yourwebsite.com" 104 /> 105 {errors.website && ( 106 <p className="text-red-500 text-sm mt-1">{errors.website.message}</p> 107 )} 108 </div> 109 110 <div> 111 <label className="block text-sm font-medium mb-2">Location</label> 112 <input 113 {...register('location')} 114 className="w-full px-3 py-2 border rounded-lg" 115 placeholder="San Francisco, CA" 116 /> 117 </div> 118 119 <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> 120 <div> 121 <label className="block text-sm font-medium mb-2">Twitter</label> 122 <input 123 {...register('twitter')} 124 className="w-full px-3 py-2 border rounded-lg" 125 placeholder="@username" 126 /> 127 </div> 128 <div> 129 <label className="block text-sm font-medium mb-2">GitHub</label> 130 <input 131 {...register('github')} 132 className="w-full px-3 py-2 border rounded-lg" 133 placeholder="username" 134 /> 135 </div> 136 <div> 137 <label className="block text-sm font-medium mb-2">LinkedIn</label> 138 <input 139 {...register('linkedin')} 140 className="w-full px-3 py-2 border rounded-lg" 141 placeholder="profile-url" 142 /> 143 </div> 144 </div> 145 146 <button 147 type="submit" 148 disabled={isLoading || !isDirty} 149 className="w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50" 150 > 151 {isLoading ? 'Saving...' : 'Save Profile'} 152 </button> 153 </form> 154 ); 155}

Checkpoint: Profile page and API working.

Step 5: Test Phase#

The workflow moves to testing with testing-expert.

Ask the testing-expert:

Write tests for the profile API and form component.

Create API tests:

1// app/api/user/profile/route.test.ts 2import { describe, it, expect, vi, beforeEach } from 'vitest'; 3 4describe('/api/user/profile', () => { 5 describe('GET', () => { 6 it('returns 401 when not authenticated', async () => { 7 // Mock unauthenticated request 8 const response = await fetch('/api/user/profile'); 9 expect(response.status).toBe(401); 10 }); 11 12 it('returns profile when authenticated', async () => { 13 // Mock authenticated request 14 const response = await fetch('/api/user/profile'); 15 expect(response.status).toBe(200); 16 17 const data = await response.json(); 18 expect(data.profile).toBeDefined(); 19 }); 20 }); 21 22 describe('PATCH', () => { 23 it('validates bio length', async () => { 24 const response = await fetch('/api/user/profile', { 25 method: 'PATCH', 26 body: JSON.stringify({ bio: 'a'.repeat(501) }), 27 }); 28 expect(response.status).toBe(400); 29 }); 30 31 it('updates profile successfully', async () => { 32 const response = await fetch('/api/user/profile', { 33 method: 'PATCH', 34 body: JSON.stringify({ bio: 'New bio' }), 35 }); 36 expect(response.status).toBe(200); 37 }); 38 }); 39});

Run tests:

npm run test

Checkpoint: Tests passing.

Step 6: Review Phase#

The workflow enters review with security-expert and code-review-expert.

Ask the security-expert:

Review the profile implementation for security issues.

The agent will check:

  • Authentication enforcement
  • Input validation
  • Data exposure
  • CSRF protection

Ask the code-review-expert:

Review the code quality and suggest improvements.

Make any recommended improvements.

Checkpoint: Security and code quality verified.

Step 7: Complete the Workflow#

bootspring workflow status

If all phases are complete:

Workflow complete: feature-development All phases passed: ✓ Plan ✓ Design ✓ Build ✓ Test ✓ Review Artifacts saved to: .bootspring/workflows/wf_xxx/

What You Learned#

  • Using the feature-development workflow
  • Coordinating multiple agents
  • Building with skills
  • Test-driven development
  • Security review process

Next Steps#

Complete Code#

The complete code for this tutorial is available at: github.com/bootspring/tutorials/first-feature