onlineArkadyuti Sarkar
    >home>blog>projects>resume>about

    Building a Full-Stack Portfolio with Next.js 15, MongoDB, and MinIO

    A
    Arkadyuti Sarkar
    9 min read21 views
    $ cat ./posts/building-a-full-stack-portfolio-with-next-js-15-mongodb-and-minio.md
    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

    Share this post: