Introduction
Building a personal portfolio is a rite of passage for every developer. But instead of a static site, I wanted something dynamic — a full-stack application with a proper CMS where I can update my projects, skills, and blog posts from an admin dashboard.
Tech Stack Overview
After researching the modern ecosystem, I settled on:
- Frontend: Next.js 14 (App Router) + Tailwind CSS + shadcn/ui + Framer Motion
- Backend: Java Spring Boot 3.4.5 + Spring Security + JWT
- Database: PostgreSQL on Neon (serverless)
- Deployment: Vercel (frontend) + Fly.io (backend)
Why Next.js App Router?
The App Router in Next.js 14 is a game-changer. Server Components allow us to fetch data on the server without shipping extra JavaScript to the client. This means blazing-fast page loads and excellent SEO — perfect for a portfolio site.
// Server Component — zero client-side JS for data fetching
export default async function HomePage() {
const posts = await fetchData('/api/blog-posts');
return <BlogSection posts={posts} />;
}
JWT Authentication with Spring Security
For the admin panel, I implemented JWT-based authentication. The token is stored in an httpOnly cookie — never in localStorage — which prevents XSS attacks from stealing the token.
@PostMapping("/auth/login")
public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
// authenticate user
String token = jwtTokenProvider.generateToken(authentication);
ResponseCookie cookie = ResponseCookie.from("jwt", token)
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(Duration.ofDays(7))
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new AuthResponse("Login successful", "admin", "ADMIN"));
}
Eliminating CORS with API Proxy
By configuring Next.js rewrites, all /api/* requests are proxied to the backend. This means the browser only ever talks to Vercel — no CORS configuration needed!
Conclusion
The result is a fast, secure, and maintainable portfolio with a fully functional admin CMS. The separation of concerns between frontend and backend makes it easy to iterate on each independently.