Building a Full-Stack Portfolio with Next.js 15, MongoDB, and MinIO
As a Frontend Associate Architect, I wanted to create a portfolio that not only showcases my work but also demonstrates modern web development practices. This technical deep-dive explores how I built a comprehensive portfolio application using Next.js 15, MongoDB, MinIO, and a custom authentication system.
Architecture Overview
This portfolio application is built with a modern tech stack designed for performance, scalability, and developer experience:
Frontend: Next.js 15 with App Router and TypeScript
Styling: Tailwind CSS with Radix UI primitives and custom components
Database: MongoDB with Mongoose ODM
File Storage: MinIO for image management
Authentication: Custom React Context-based mock authentication
State Management: React Context for authentication state
Content Management: Rich text editor with BlockNote
Key Features Implemented
1. Modern Next.js 15 Implementation
The application leverages Next.js 15's App Router with full TypeScript support. One of the key challenges was handling the new Promise-based params and searchParams:
// Dynamic route handling in Next.js 15
export default async function BlogPost({
params
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
// Server-side data fetching
const blog = await getBlogBySlug(slug)
return <BlogContent blog={blog} />
}2. Database Architecture with MongoDB
The application uses MongoDB with a well-structured schema system. Each data model includes Zod validation for type safety:
// Blog schema with automatic slug generation
export const blogSchema = z.object({
id: z.string(),
publishedAt: z.number(),
title: z.string(),
excerpt: z.string(),
coverImage: z.string(),
author: z.string(),
slug: z.string(),
content: z.string(),
contentRTE: z.unknown(),
contentImages: z.array(z.string()).optional(),
tags: z.array(tagSchema),
featured: z.boolean(),
isDraft: z.boolean().optional(),
})3. Custom Authentication System
The application implements a simple mock authentication system using React Context for development and demonstration purposes:
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
const login = async (email: string, password: string): Promise<boolean> => {
// Mock authentication for demonstration
if (email === 'admin@example.com' && password === 'password') {
const userData = { id: '1', email, name: 'Admin User' }
setUser(userData)
localStorage.setItem('adminUser', JSON.stringify(userData))
return true
}
return false
}
}4. Image Management with MinIO
One of the most interesting challenges was implementing a robust image management system. The application uses MinIO for object storage with automatic cleanup:
// Image upload handling in blog editor
const handleImageUpload = async (file: File): Promise<string> => {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/blog-content-image', {
method: 'POST',
body: formData,
})
const { data } = await response.json()
return data.url
}5. Rich Text Editor Integration with BlockNote
One of the most interesting technical decisions was choosing BlockNote over traditional rich text editors like TinyMCE, CKEditor, or Quill. BlockNote is a modern, block-based editor similar to Notion, which provides several advantages for content management:
Why BlockNote?
Block-Based Architecture: Unlike traditional WYSIWYG editors that work with HTML directly, BlockNote uses a block-based approach where each piece of content (paragraph, heading, image, etc.) is a structured block. This provides:
Predictable Output: No more cleaning up messy HTML from copy-paste operations
Structured Data: Content is stored as JSON blocks, making it easier to transform and validate
Type Safety: Full TypeScript support with strongly-typed block definitions
Modern UX: Familiar interface similar to Notion, Craft, or Bear
Implementation Details:
The BlockNote integration includes sophisticated features like automatic image management and content tracking:
export default function BlockNoteEditorLocal({
onDataChange,
initialContent,
onImageUpload,
onImageDelete,
}: BlockNoteEditorLocalProps) {
const editor = useCreateBlockNote({
initialContent: initialContent,
uploadFile: async (file: File) => {
// Custom upload handler integrates with MinIO
const url = await uploadFile(file)
if (onImageUpload) {
onImageUpload(url) // Track uploaded images
}
return url
},
})
const handleOnChange = async () => {
const blocks = editor.document
const html = await editor.blocksToFullHTML(blocks)
// Intelligent image cleanup - detect deleted images
if (onImageDelete && initialContent) {
const currentImages = blocks
.filter((block) => block.type === 'image')
.map((block) => block.props.url)
const initialImages = initialContent
.filter((block) => block.type === 'image')
.map((block) => block.props.url)
// Clean up orphaned images from MinIO
const deletedImages = initialImages.filter(url => !currentImages.includes(url))
deletedImages.forEach(url => onImageDelete(url))
}
onDataChange(blocks, html)
}
return <BlockNoteView onChange={handleOnChange} editor={editor} />
}Advanced Features Implemented
1. Automatic Image Management: The editor automatically tracks image uploads and deletions, ensuring no orphaned files remain in MinIO storage.
2. Dual Content Storage: Content is stored both as structured JSON blocks (contentRTE) and rendered HTML (content) for flexibility:
// In the blog form submission
formData.append('contentRTE', JSON.stringify(editorRef.current.contentRTE))
formData.append('content', editorRef.current.content)
formData.append('contentImages', JSON.stringify(editorRef.current.contentImages))3. Error Handling: Graceful fallback when loading malformed content:
try {
editor = useCreateBlockNote({
initialContent: initialContent,
uploadFile: uploadHandler
})
} catch (error) {
// Fallback to empty editor if content is corrupted
editor = useCreateBlockNote({
initialContent: undefined,
uploadFile: uploadHandler
})
}4. Real-time Content Sync: Changes are immediately reflected in the parent form state, enabling auto-save functionality and live previews.
Integration with MinIO Storage
The editor seamlessly integrates with the MinIO file storage system:
async function uploadFile(file: File) {
const body = new FormData()
body.append('file', file)
const response = await fetch('/api/blog-content-image', {
method: 'POST',
body: body,
})
const result = await response.json()
if (!result.success) {
throw new Error(result.message || 'Upload failed')
}
return result.data.url // Direct MinIO URL
}Benefits Over Traditional Editors
Compared to traditional rich text editors, this BlockNote implementation provides:
No HTML Cleanup: Block-based structure prevents malformed HTML
Better Performance: Lighter weight than heavy WYSIWYG editors
Modern UX: Slash commands, drag-and-drop, and keyboard shortcuts
Extensibility: Easy to add custom block types for specific content needs
Mobile-First: Touch-friendly interface that works well on all devices
Accessibility: Built-in ARIA support and keyboard navigation
This choice demonstrates how modern block-based editors can provide a superior content creation experience while maintaining clean, structured data storage.
Current Features
Search Functionality
The application includes a comprehensive search system that works across blogs and projects:
// Server-side search implementation
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>
}) {
const params = await searchParams
const searchQuery = params.q || ''
if (searchQuery) {
await connectToDatabase()
const searchRegex = { $regex: searchQuery, $options: 'i' }
// Search blogs and projects
const blogs = await BlogModels.find({
$or: [
{ title: searchRegex },
{ excerpt: searchRegex },
{ content: searchRegex }
],
isDraft: false
})
}
}Newsletter Integration
The portfolio includes a complete newsletter signup system:
// Newsletter form component
export default function NewsletterForm() {
const [email, setEmail] = useState('')
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const response = await fetch('/api/newsletter', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
})
}
}Performance Optimizations
Server-Side Rendering and SEO
The application implements comprehensive SEO with Next.js metadata API:
export const metadata: Metadata = {
metadataBase: new URL(siteMetadata.siteUrl),
title: {
default: siteMetadata.title,
template: `%s | ${siteMetadata.title.split('|')[0].trim()}`,
},
description: siteMetadata.description,
openGraph: {
title: siteMetadata.title,
description: siteMetadata.description,
images: [siteMetadata.socialBanner],
type: 'website',
},
twitter: {
card: 'summary_large_image',
images: [siteMetadata.socialBanner],
}
}Image Optimization
All images are processed through Next.js Image component with optimized loading:
<Image
src={blog.coverImage}
alt={blog.title}
width={600}
height={400}
className="rounded-lg object-cover"
priority={featured}
/>API Design and Data Flow
RESTful API Structure
The application follows RESTful principles with consistent response formatting:
interface ApiResponse<T> {
success: boolean
data?: T
error?: {
message: string
code: string
}
}
// Example API endpoint
export async function GET(request: NextRequest) {
try {
await connectToDatabase()
const blogs = await BlogModels.find({ isDraft: false })
.sort({ publishedAt: -1 })
.limit(10)
return NextResponse.json({
success: true,
data: transformToBlogs(blogs)
})
} catch (error) {
return NextResponse.json({
success: false,
error: { message: 'Failed to fetch blogs', code: 'FETCH_ERROR' }
}, { status: 500 })
}
}Server-Side Data Fetching
The application uses Next.js App Router's server-side data fetching:
// Server component data fetching
async function getRecentBlogPosts() {
await connectToDatabase()
const blogs = await BlogModels.find({ isDraft: false })
.sort({ publishedAt: -1 })
.limit(3)
.lean()
return transformToBlogs(blogs)
}Deployment and DevOps
Docker Containerization
The application includes a complete Docker setup:
FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["yarn", "start"]Tech Stack Summary
The application successfully combines:
Next.js 15 with App Router for modern React development
TypeScript for type safety throughout the stack
MongoDB with Mongoose for data persistence
Radix UI primitives with Tailwind CSS for accessible components
MinIO for scalable file storage
BlockNote for rich text editing
Zod for runtime validation
Lessons Learned
1. Server-First Architecture
Next.js 15's App Router encourages server-side data fetching, which provides better performance and SEO out of the box.
2. Component Architecture
Using Radix UI primitives with custom styling provides the best of both worlds - accessibility and customization.
3. Type Safety is Crucial
Using TypeScript with Zod validation eliminated entire classes of runtime errors.
Future Enhancements
Planned Technical Improvements
Production Authentication: Implement JWT-based authentication for production use
Caching Strategy: Add Redis for improved performance
CDN Integration: CloudFront for global content delivery
Monitoring: Application performance monitoring with real-time alerts
Testing: Comprehensive test suite with Playwright and Jest
Conclusion
Building this portfolio application provided valuable insights into modern full-stack development with Next.js 15. The combination of server-side rendering, comprehensive type safety, and robust image management creates a scalable foundation for content-driven applications.
The mock authentication system demonstrates the architecture for secure admin functionality, while the search and newsletter features show practical implementations of common web application requirements.
Repository: GitHub Live Demo: https://dev.visharka.us