A personal website is more than just a portfolio it's a playground for experimenting with architecture, performance, and production-ready engineering.
Introduction
Ask ten software engineers what their personal website is for, and you'll probably hear the same answer: "It's my portfolio."
While that's true, I wanted mine to be something more.
I wanted shubhkumar.in to be a platform that could grow with me a place to showcase projects, host my CV, publish technical blogs, and experiment with ideas before applying them in production systems.
Rather than using a static template or website builder, I decided to build everything from scratch. My goal wasn't to use the most technologies possible; it was to create a clean architecture that was fast, maintainable, and easy to extend.
Today, the website consists of two main parts:
A Next.js frontend deployed on Vercel
A Node.js + Express backend deployed on Render
Behind the scenes, MongoDB stores dynamic content, while Redis speeds up API responses through caching.
It sounds like a fairly standard stack and in many ways, it is. But the interesting part wasn't choosing the technologies. It was designing how they work together.
Why Build It From Scratch?
There are countless templates and portfolio generators available today. They look great, take minutes to deploy, and require almost no maintenance.
So why spend time building everything yourself?
For me, the answer was simple.
I wanted complete control.
Not just over the design, but over the architecture.
I wanted a backend that wasn't tightly coupled to a frontend. I wanted my content to live in one place instead of being duplicated across pages. Most importantly, I wanted a project that reflected how I build software professionally.
Every new feature became an opportunity to solve a real engineering problem instead of simply adding another section to a webpage.
The Tech Stack
I deliberately kept the stack simple.
Frontend
Next.js
Tailwind CSS
Hosted on Vercel
Next.js gives me everything I need for a modern website:
Server Components
Static rendering
Excellent SEO
Fast routing
Built-in image optimization
Incremental Static Regeneration (ISR)
Tailwind CSS keeps styling consistent without maintaining a large CSS codebase.
Deploying to Vercel makes the frontend almost effortless. Every push automatically builds and deploys the latest version.
Backend
Instead of relying on Next.js API routes, I built a dedicated backend using:
Node.js
Express
Hosted on Render
This backend acts as the single source of truth for all dynamic content.
Whether it's:
portfolio information
experience
projects
resume data
future APIs
everything is served from one backend.
Keeping the backend independent means it can later power:
a mobile app
an admin dashboard
CLI tools
browser extensions
or any future frontend
without changing business logic.
Database
Dynamic content is stored in MongoDB.
Documents include:
Profile information
Experience
Skills
Projects
Portfolio data
Interestingly, blog posts are not stored in MongoDB. They are maintained separately, allowing the website content and blog content to evolve independently.
Caching
To reduce unnecessary database queries, the Express API caches responses in Redis.
The flow looks like this:
Client
│
▼
Express API
│
▼
Redis
│
Cache Hit?
│
┌─┴─────────────┐
│ │
Yes No
│ │
▼ ▼
Return MongoDB
Response │
▼
Store in Redis
│
▼
Return Response
Most requests never reach MongoDB.
This keeps API responses fast while reducing database load.
Overall Architecture
At a high level, the system looks like this.
+----------------------+
| Next.js |
| Hosted on Vercel |
+----------+-----------+
|
|
HTTP Requests
|
▼
+----------------------+
| Express Backend |
| Hosted on Render |
+----------+-----------+
|
+-----------+-----------+
| |
▼ ▼
Redis Cache MongoDB Atlas
Although there are multiple services, each one has a single responsibility.
Next.js renders pages.
Express serves business logic.
Redis caches responses.
MongoDB stores data.
Keeping responsibilities separate makes the system easier to reason about and easier to extend.
Why I Didn't Use Next.js API Routes
This was probably the architectural decision that influenced the project the most.
Many Next.js applications place all backend logic directly inside API routes.
There's absolutely nothing wrong with that approach.
But I wanted something reusable.
By separating the backend, the frontend becomes just another client.
Tomorrow, if I decide to build:
an Android app
an iOS app
a desktop application
another website
they can all consume the exact same API.
No duplicated logic.
No duplicated validation.
No duplicated database queries.
Everything lives in one place.
Making the Backend the Single Source of Truth
The homepage.
The portfolio.
The CV.
Future applications.
All of them consume the same backend.
Instead of every page maintaining its own copy of data, everything originates from one API.
Updating my experience in MongoDB automatically updates every place where it's displayed.
This significantly reduces maintenance and prevents data from going out of sync.
The Unexpected Problem: Cache Invalidation
The most interesting problem wasn't building the website.
It was keeping it fresh.
Initially, everything looked perfect.
MongoDB stored the latest content.
Redis cached API responses.
Next.js generated static pages.
Performance was excellent.
Yet something strange happened.
Whenever I updated content, users didn't always see the changes immediately.
Sometimes it took several seconds.
Sometimes much longer.
At first, I assumed Redis was serving stale data.
After debugging for a while, I realized Redis wasn't the problem at all.
The real issue was that there were two completely independent caching layers.
The API cache and the frontend cache.
The API could already have fresh data while Next.js continued serving previously generated pages.
Everything was technically working exactly as intended.
The architecture, however, wasn't.
Solving It with Event-Driven Revalidation
Instead of waiting for caches to expire naturally, I switched to an event-driven approach.
Whenever content changes, the following sequence happens:
Admin API
│
▼
Update MongoDB
│
▼
Trigger Next.js Revalidation
│
▼
Flush Redis Cache
│
▼
Users receive fresh content
This means content updates propagate almost immediately without waiting for cache expiration.
The important lesson here is that caching is only half the problem.
The other half is knowing exactly when to invalidate that cache.
Designing a reliable invalidation strategy is often harder than adding caching in the first place.
Hosting Strategy
Keeping the frontend and backend separate also simplified deployment.
Frontend
The Next.js application is deployed on Vercel.
Benefits include:
Automatic deployments
Preview environments
Global CDN
Optimized image delivery
Backend
The Express server runs independently on Render.
Separating deployments means I can:
deploy backend fixes without rebuilding the frontend
deploy UI updates without touching backend services
scale each independently in the future
Lessons Learned
Building this website taught me several lessons that extend far beyond personal projects.
1. Simplicity scales
A small, well-structured architecture is easier to maintain than an unnecessarily complex one.
2. Separate responsibilities
Frontend rendering, backend logic, caching, and persistence all have different jobs.
Keeping those responsibilities isolated makes the system easier to evolve.
3. Build reusable APIs
The backend shouldn't exist solely for one website.
Treating it as a standalone service opens the door for future applications without additional work.
4. Cache invalidation deserves as much attention as caching
Adding Redis is easy.
Designing when and how cached data should be refreshed is where the real engineering begins.
5. Personal projects are the best place to experiment
Production systems often have strict requirements.
Personal projects provide the freedom to test ideas, refine architectures, and learn from mistakes.
Many of the lessons learned while building this website are directly applicable to larger production systems.
What's Next?
The website continues to evolve.
Some ideas I'm exploring include:
A richer admin experience for managing content
Better analytics and monitoring
Search functionality
AI-powered features
Additional APIs for future projects
More automation around content publishing
Because the architecture is modular, adding new capabilities doesn't require rewriting existing components.
That's exactly how I wanted the system to grow.
Final Thoughts
Building shubhkumar.in wasn't about creating another portfolio website.
It was about building a platform that reflects how I think about software engineering.
Choosing Next.js, Tailwind CSS, Node.js, Express, MongoDB, and Redis wasn't about following trends. It was about selecting tools that work well together while keeping the architecture clean and maintainable.
The biggest lesson wasn't learning a new framework or deploying another application.
It was realizing that good architecture isn't defined by how many technologies you use.
It's defined by how clearly each piece of the system is responsible for one job and how well those pieces work together.
If there's one takeaway I'd leave you with, it's this:
Treat your personal projects like production systems. Not because they need enterprise-scale complexity, but because they're the best place to learn the engineering practices you'll eventually use in production.
After all, the best portfolio isn't the one with the fanciest animations it's the one that demonstrates how you think as an engineer.
Top comments (0)