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-developmentWhen 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 uploadCheckpoint: 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 pushAPI 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-crudCreate 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 testCheckpoint: 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 statusIf 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