27 KiB
27 KiB
SiliconPin Code Patterns and Examples
This document provides common patterns and examples for extending the SiliconPin platform.
Table of Contents
- Authentication Patterns
- API Route Patterns
- Database Patterns
- Form Handling
- Component Patterns
- State Management
- Error Handling
Authentication Patterns
Using Authentication in Components
// components/ProfileCard.tsx
'use client';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
export function ProfileCard() {
const { user, isLoading, logout } = useAuth();
if (isLoading) {
return <div>Loading...</div>;
}
if (!user) {
return <div>Please log in to view your profile.</div>;
}
return (
<Card>
<CardHeader>
<CardTitle>Welcome, {user.name}!</CardTitle>
</CardHeader>
<CardContent>
<p>Email: {user.email}</p>
<p>Member since: {new Date(user.createdAt).toLocaleDateString()}</p>
<Button onClick={logout} variant="outline">
Sign Out
</Button>
</CardContent>
</Card>
);
}
Protected Route Component
// components/ProtectedRoute.tsx
'use client';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
interface ProtectedRouteProps {
children: React.ReactNode;
redirectTo?: string;
}
export function ProtectedRoute({
children,
redirectTo = '/auth'
}: ProtectedRouteProps) {
const { user, isLoading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isLoading && !user) {
router.push(redirectTo);
}
}, [user, isLoading, router, redirectTo]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div>Loading...</div>
</div>
);
}
if (!user) {
return null; // Will redirect
}
return <>{children}</>;
}
API Route Patterns
Basic CRUD API Route
// app/api/posts/route.ts
import { authMiddleware } from '@/lib/auth-middleware'
import { connectDB } from '@/lib/mongodb'
import { Post } from '@/models/post'
import { z } from 'zod'
const CreatePostSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(10, 'Content must be at least 10 characters'),
tags: z.array(z.string()).optional(),
})
// GET /api/posts - Get all posts
export async function GET(request: Request) {
try {
await connectDB()
const url = new URL(request.url)
const page = parseInt(url.searchParams.get('page') || '1')
const limit = parseInt(url.searchParams.get('limit') || '10')
const skip = (page - 1) * limit
const posts = await Post.find()
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.populate('author', 'name email')
const total = await Post.countDocuments()
return Response.json({
success: true,
data: {
posts,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
},
})
} catch (error) {
return Response.json({ success: false, error: 'Failed to fetch posts' }, { status: 500 })
}
}
// POST /api/posts - Create new post
export async function POST(request: Request) {
try {
const user = await authMiddleware(request)
if (!user) {
return Response.json({ success: false, error: 'Authentication required' }, { status: 401 })
}
const body = await request.json()
const validatedData = CreatePostSchema.parse(body)
await connectDB()
const post = new Post({
...validatedData,
author: user.id,
})
await post.save()
await post.populate('author', 'name email')
return Response.json(
{
success: true,
data: { post },
message: 'Post created successfully',
},
{ status: 201 }
)
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json(
{
success: false,
error: 'Validation failed',
details: error.errors.map((err) => ({
field: err.path.join('.'),
message: err.message,
})),
},
{ status: 400 }
)
}
return Response.json({ success: false, error: 'Failed to create post' }, { status: 500 })
}
}
Dynamic API Route with ID
// app/api/posts/[id]/route.ts
import { authMiddleware } from '@/lib/auth-middleware'
import { connectDB } from '@/lib/mongodb'
import { Post } from '@/models/post'
import mongoose from 'mongoose'
interface RouteParams {
params: {
id: string
}
}
// GET /api/posts/[id] - Get single post
export async function GET(request: Request, { params }: RouteParams) {
try {
if (!mongoose.Types.ObjectId.isValid(params.id)) {
return Response.json({ success: false, error: 'Invalid post ID' }, { status: 400 })
}
await connectDB()
const post = await Post.findById(params.id).populate('author', 'name email')
if (!post) {
return Response.json({ success: false, error: 'Post not found' }, { status: 404 })
}
return Response.json({
success: true,
data: { post },
})
} catch (error) {
return Response.json({ success: false, error: 'Failed to fetch post' }, { status: 500 })
}
}
// DELETE /api/posts/[id] - Delete post
export async function DELETE(request: Request, { params }: RouteParams) {
try {
const user = await authMiddleware(request)
if (!user) {
return Response.json({ success: false, error: 'Authentication required' }, { status: 401 })
}
if (!mongoose.Types.ObjectId.isValid(params.id)) {
return Response.json({ success: false, error: 'Invalid post ID' }, { status: 400 })
}
await connectDB()
const post = await Post.findById(params.id)
if (!post) {
return Response.json({ success: false, error: 'Post not found' }, { status: 404 })
}
// Check if user owns the post
if (post.author.toString() !== user.id) {
return Response.json(
{ success: false, error: 'Not authorized to delete this post' },
{ status: 403 }
)
}
await Post.findByIdAndDelete(params.id)
return Response.json({
success: true,
message: 'Post deleted successfully',
})
} catch (error) {
return Response.json({ success: false, error: 'Failed to delete post' }, { status: 500 })
}
}
Database Patterns
Mongoose Model with Validation
// models/post.ts
import mongoose, { Document, Schema } from 'mongoose'
import { z } from 'zod'
// Zod schema for validation
export const PostValidationSchema = z.object({
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
content: z.string().min(10, 'Content must be at least 10 characters'),
tags: z.array(z.string()).max(10, 'Maximum 10 tags allowed').optional(),
published: z.boolean().default(false),
})
// TypeScript interface
export interface IPost extends Document {
title: string
content: string
tags?: string[]
published: boolean
author: mongoose.Types.ObjectId
createdAt: Date
updatedAt: Date
}
// Mongoose schema
const PostSchema = new Schema<IPost>(
{
title: {
type: String,
required: [true, 'Title is required'],
trim: true,
maxlength: [200, 'Title cannot exceed 200 characters'],
},
content: {
type: String,
required: [true, 'Content is required'],
minlength: [10, 'Content must be at least 10 characters'],
},
tags: [
{
type: String,
trim: true,
lowercase: true,
},
],
published: {
type: Boolean,
default: false,
},
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
},
{
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
}
)
// Indexes for performance
PostSchema.index({ author: 1, createdAt: -1 })
PostSchema.index({ published: 1, createdAt: -1 })
PostSchema.index({ tags: 1 })
// Virtual for excerpt
PostSchema.virtual('excerpt').get(function () {
return this.content.length > 150 ? this.content.substring(0, 150) + '...' : this.content
})
export const Post = mongoose.models.Post || mongoose.model<IPost>('Post', PostSchema)
Database Connection with Error Handling
// lib/mongodb.ts (enhanced)
import mongoose from 'mongoose'
const MONGODB_URI = process.env.MONGODB_URI
if (!MONGODB_URI) {
throw new Error('Please define the MONGODB_URI environment variable')
}
interface CachedConnection {
conn: typeof mongoose | null
promise: Promise<typeof mongoose> | null
}
// Cache connection in development to avoid connection limit
declare global {
var mongoose: CachedConnection | undefined
}
let cached: CachedConnection = global.mongoose || { conn: null, promise: null }
if (!global.mongoose) {
global.mongoose = cached
}
export async function connectDB(): Promise<typeof mongoose> {
if (cached.conn) {
return cached.conn
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
family: 4, // Use IPv4, skip trying IPv6
}
cached.promise = mongoose.connect(MONGODB_URI!, opts)
}
try {
cached.conn = await cached.promise
console.log('MongoDB connected successfully')
return cached.conn
} catch (error) {
cached.promise = null
console.error('MongoDB connection error:', error)
throw error
}
}
// Graceful shutdown
process.on('SIGINT', async () => {
if (cached.conn) {
await cached.conn.disconnect()
console.log('MongoDB disconnected through app termination')
process.exit(0)
}
})
Form Handling
Form with Validation and Error Handling
// components/CreatePostForm.tsx
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
const CreatePostSchema = z.object({
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
content: z.string().min(10, 'Content must be at least 10 characters'),
tags: z.string().optional(),
});
type CreatePostForm = z.infer<typeof CreatePostSchema>;
export function CreatePostForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const queryClient = useQueryClient();
const form = useForm<CreatePostForm>({
resolver: zodResolver(CreatePostSchema),
defaultValues: {
title: '',
content: '',
tags: '',
},
});
const createPostMutation = useMutation({
mutationFn: async (data: CreatePostForm) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
...data,
tags: data.tags?.split(',').map(tag => tag.trim()).filter(Boolean),
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to create post');
}
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
form.reset();
},
});
const onSubmit = async (data: CreatePostForm) => {
try {
setIsSubmitting(true);
await createPostMutation.mutateAsync(data);
} catch (error) {
console.error('Error creating post:', error);
} finally {
setIsSubmitting(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle>Create New Post</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div>
<Label htmlFor="title">Title</Label>
<Input
id="title"
{...form.register('title')}
className={form.formState.errors.title ? 'border-red-500' : ''}
/>
{form.formState.errors.title && (
<p className="text-red-500 text-sm mt-1">
{form.formState.errors.title.message}
</p>
)}
</div>
<div>
<Label htmlFor="content">Content</Label>
<Textarea
id="content"
rows={8}
{...form.register('content')}
className={form.formState.errors.content ? 'border-red-500' : ''}
/>
{form.formState.errors.content && (
<p className="text-red-500 text-sm mt-1">
{form.formState.errors.content.message}
</p>
)}
</div>
<div>
<Label htmlFor="tags">Tags (comma-separated)</Label>
<Input
id="tags"
placeholder="react, nextjs, typescript"
{...form.register('tags')}
/>
</div>
{createPostMutation.error && (
<div className="text-red-500 text-sm">
Error: {createPostMutation.error.message}
</div>
)}
<Button
type="submit"
disabled={isSubmitting || createPostMutation.isPending}
className="w-full"
>
{isSubmitting || createPostMutation.isPending ? 'Creating...' : 'Create Post'}
</Button>
</form>
</CardContent>
</Card>
);
}
Component Patterns
Data Fetching Component
// components/PostsList.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
interface Post {
id: string;
title: string;
excerpt: string;
author: {
name: string;
};
createdAt: string;
tags: string[];
}
interface PostsResponse {
success: boolean;
data: {
posts: Post[];
pagination: {
page: number;
total: number;
pages: number;
};
};
}
interface PostsListProps {
page?: number;
limit?: number;
}
export function PostsList({ page = 1, limit = 10 }: PostsListProps) {
const {
data,
isLoading,
error,
refetch,
} = useQuery<PostsResponse>({
queryKey: ['posts', page, limit],
queryFn: async () => {
const response = await fetch(
`/api/posts?page=${page}&limit=${limit}`,
{ credentials: 'include' }
);
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
return response.json();
},
staleTime: 5 * 60 * 1000, // 5 minutes
});
if (isLoading) {
return (
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-6 w-3/4" />
</CardHeader>
<CardContent>
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-2/3" />
</CardContent>
</Card>
))}
</div>
);
}
if (error) {
return (
<Card>
<CardContent className="p-6 text-center">
<p className="text-red-500 mb-4">
Error loading posts: {error.message}
</p>
<Button onClick={() => refetch()}>
Try Again
</Button>
</CardContent>
</Card>
);
}
if (!data?.data.posts.length) {
return (
<Card>
<CardContent className="p-6 text-center">
<p>No posts found.</p>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
{data.data.posts.map((post) => (
<Card key={post.id}>
<CardHeader>
<CardTitle className="line-clamp-2">{post.title}</CardTitle>
<div className="text-sm text-gray-500">
By {post.author.name} • {new Date(post.createdAt).toLocaleDateString()}
</div>
</CardHeader>
<CardContent>
<p className="text-gray-700 mb-3 line-clamp-3">{post.excerpt}</p>
{post.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="px-2 py-1 text-xs bg-gray-100 rounded-full"
>
{tag}
</span>
))}
</div>
)}
</CardContent>
</Card>
))}
</div>
);
}
State Management
Custom Hook for API Data
// hooks/usePosts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
interface Post {
id: string
title: string
content: string
author: {
id: string
name: string
}
createdAt: string
updatedAt: string
}
interface CreatePostData {
title: string
content: string
tags?: string[]
}
export function usePosts(page = 1, limit = 10) {
return useQuery({
queryKey: ['posts', page, limit],
queryFn: async () => {
const response = await fetch(`/api/posts?page=${page}&limit=${limit}`, {
credentials: 'include',
})
if (!response.ok) {
throw new Error('Failed to fetch posts')
}
return response.json()
},
})
}
export function usePost(id: string) {
return useQuery({
queryKey: ['posts', id],
queryFn: async () => {
const response = await fetch(`/api/posts/${id}`, {
credentials: 'include',
})
if (!response.ok) {
throw new Error('Failed to fetch post')
}
return response.json()
},
enabled: !!id,
})
}
export function useCreatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (data: CreatePostData) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to create post')
}
return response.json()
},
onSuccess: () => {
// Invalidate and refetch posts list
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
}
export function useDeletePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string) => {
const response = await fetch(`/api/posts/${id}`, {
method: 'DELETE',
credentials: 'include',
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to delete post')
}
return response.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
}
Error Handling
Global Error Boundary
// components/ErrorBoundary.tsx
'use client';
import React from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
interface ErrorBoundaryProps {
children: React.ReactNode;
fallback?: React.ComponentType<{ error: Error; reset: () => void }>;
}
export class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
const reset = () => this.setState({ hasError: false, error: undefined });
if (this.props.fallback) {
const FallbackComponent = this.props.fallback;
return <FallbackComponent error={this.state.error!} reset={reset} />;
}
return (
<Card className="max-w-md mx-auto mt-8">
<CardHeader>
<CardTitle className="text-red-600">Something went wrong</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 mb-4">
We apologize for the inconvenience. Please try refreshing the page.
</p>
<div className="space-x-2">
<Button onClick={reset}>Try Again</Button>
<Button
variant="outline"
onClick={() => window.location.reload()}
>
Refresh Page
</Button>
</div>
</CardContent>
</Card>
);
}
return this.props.children;
}
}
// Usage in layout
export function Layout({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary>
{children}
</ErrorBoundary>
);
}
API Error Utility
// lib/api-error.ts
export class APIError extends Error {
constructor(
message: string,
public status: number,
public code?: string
) {
super(message)
this.name = 'APIError'
}
}
export function handleAPIError(error: unknown): APIError {
if (error instanceof APIError) {
return error
}
if (error instanceof Error) {
return new APIError(error.message, 500)
}
return new APIError('An unknown error occurred', 500)
}
// Usage in API routes
export async function POST(request: Request) {
try {
// Your API logic
} catch (error) {
const apiError = handleAPIError(error)
return Response.json(
{
success: false,
error: apiError.message,
code: apiError.code,
},
{ status: apiError.status }
)
}
}
SiliconPin-Specific Patterns
Topic Management Pattern
// hooks/useTopics.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
export function useTopics(searchTerm?: string, tags?: string[]) {
return useQuery({
queryKey: ['topics', searchTerm, tags],
queryFn: async () => {
const params = new URLSearchParams()
if (searchTerm) params.set('q', searchTerm)
if (tags?.length) params.set('tags', tags.join(','))
const response = await fetch(`/api/topics?${params}`, {
credentials: 'include',
})
if (!response.ok) throw new Error('Failed to fetch topics')
return response.json()
},
})
}
export function useCreateTopic() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (data: CreateTopicData) => {
const response = await fetch('/api/topics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to create topic')
}
return response.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['topics'] })
},
})
}
Admin Dashboard Pattern
// components/admin/AdminLayout.tsx
'use client'
import { useAuth } from '@/contexts/AuthContext'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
interface AdminLayoutProps {
children: React.ReactNode
}
export function AdminLayout({ children }: AdminLayoutProps) {
const { user, isLoading } = useAuth()
const router = useRouter()
useEffect(() => {
if (!isLoading && (!user || user.role !== 'admin')) {
router.push('/auth')
}
}, [user, isLoading, router])
if (isLoading) {
return <div>Loading admin panel...</div>
}
if (!user || user.role !== 'admin') {
return null
}
return (
<div className="admin-layout">
<aside className="admin-sidebar">
{/* Admin Navigation */}
</aside>
<main className="admin-content">
{children}
</main>
</div>
)
}
Payment Integration Pattern
// hooks/usePayments.ts
export function useInitiatePayment() {
return useMutation({
mutationFn: async (data: PaymentData) => {
const response = await fetch('/api/payments/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Payment initiation failed')
}
const result = await response.json()
// Redirect to payment gateway
if (result.data.paymentUrl) {
window.location.href = result.data.paymentUrl
}
return result
},
})
}
export function useUserBalance() {
return useQuery({
queryKey: ['user', 'balance'],
queryFn: async () => {
const response = await fetch('/api/user/balance', {
credentials: 'include',
})
if (!response.ok) throw new Error('Failed to fetch balance')
return response.json()
},
refetchInterval: 30000, // Refetch every 30 seconds
})
}
Service Deployment Pattern
// hooks/useServices.ts
export function useDeployService() {
return useMutation({
mutationFn: async (data: ServiceDeploymentData) => {
const endpoint = {
kubernetes: '/api/services/deploy-kubernetes',
vpn: '/api/services/deploy-vpn',
cloud: '/api/services/deploy-cloude',
}[data.type]
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Service deployment failed')
}
return response.json()
},
onSuccess: (data) => {
// Handle successful deployment
console.log('Service deployed:', data)
},
})
}
These patterns provide a solid foundation for extending the SiliconPin platform with additional features while maintaining consistency and best practices specific to the comprehensive web services platform.