Learning Next.js as a Flutter Developer
What clicked, what confused me, and the mental models that helped while building this portfolio with Next.js after years of mobile development.
I've spent most of my career building mobile apps, most recently in Flutter. When I decided to build a portfolio site, I could have used Flutter Web. Instead I picked Next.js, partly because it's the right tool for a content site and partly because I wanted to learn something outside my comfort zone.
This site, including the page you're reading, is the result. Here's what the learning curve looked like from a mobile developer's point of view: the concepts that mapped neatly onto Flutter, the ones that didn't, and the habits I'd recommend to anyone making the same move.
Why Not Just Use Flutter Web?
Flutter Web is great for app-like experiences: dashboards, internal tools, anything that behaves like a mobile app in a browser. A portfolio and blog has different needs:
- SEO and link previews. Search engines and social cards need real HTML, not a canvas.
- Fast first load. Visitors arrive from a link and decide in a couple of seconds whether to stay.
- Text-first content. Selectable text, browser find, reader mode, and accessibility work for free with HTML.
Next.js renders pages to HTML on the server (or at build time), which suits all three. Choosing the right tool for the job is part of engineering, even when it means leaving your favourite framework.
The Mental Model Map
The fastest way I found to learn Next.js was to map it onto concepts I already knew from Flutter:
| Flutter | Next.js / React |
|---|---|
Widget | Component (a function that returns JSX) |
StatelessWidget | Server Component (the default) |
StatefulWidget + setState | Client Component with useState |
| Widget constructor parameters | Props |
GoRouter route table | Folders in the app/ directory |
ShellRoute / shared scaffold | layout.tsx |
ThemeData | CSS variables + Tailwind classes |
pubspec.yaml | package.json |
flutter run + hot reload | npm run dev + Fast Refresh |
It's not a perfect mapping, but it got me productive in an afternoon. The concepts that didn't map neatly turned out to be the most interesting.
Routing Is Just Folders
In Flutter I'd define routes in code with GoRouter. In the Next.js App Router, the folder structure is the route table:
app/
├── layout.tsx # wraps every page (like a ShellRoute)
├── page.tsx # → /
└── blog/
├── page.tsx # → /blog
└── [slug]/
└── page.tsx # → /blog/learning-nextjs-as-a-flutter-developer
Square brackets mark a dynamic segment, similar to /blog/:slug in GoRouter. I didn't miss the route configuration at all. Seeing a URL tells you exactly which file renders it.
Server Components: The Biggest Mindset Shift
This is the part with no real Flutter equivalent. In Flutter, every widget runs on the device. In Next.js, components run on the server by default, so they can read files, query databases, or call APIs directly, and only the resulting HTML is sent to the browser.
The page that lists my blog posts reads Markdown files straight from disk:
// app/blog/page.tsx — a Server Component
import { getAllPosts } from "../../lib/posts";
export default function BlogIndex() {
const posts = getAllPosts(); // reads content/blog/*.md with node:fs
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
);
}
There's no API endpoint, no loading spinner, and no state management, because the component is the backend. Coming from mobile, where every piece of data crosses a network boundary through a repository, this felt almost like cheating.
The catch is that Server Components can't be interactive. No onClick, no useState, no browser APIs. For that you opt into a Client Component.
Client Components: Opting In to Interactivity
Adding "use client" at the top of a file turns it into a Client Component. This is the closest thing to a StatefulWidget:
"use client";
import { useState } from "react";
export function LikeButton() {
const [likes, setLikes] = useState(0);
return (
<button onClick={() => setLikes(likes + 1)}>
❤️ {likes}
</button>
);
}
useState feels a lot like setState: change the value, and the component rebuilds. The difference is that you return a new value rather than mutating inside a callback, which will feel familiar if you've used Bloc or Riverpod with immutable state.
The rule I settled on: keep components on the server by default, and push "use client" as far down the tree as possible. A whole page shouldn't become a Client Component just because it contains one button. Make the button the Client Component. It's the same instinct as keeping setState scoped to the smallest widget that needs it, so you don't rebuild the whole screen.
Static Generation: Build Once, Serve Everywhere
Each blog post is a page at /blog/[slug]. Rather than rendering it on every request, I tell Next.js which slugs exist so it can generate the HTML at build time:
// app/blog/[slug]/page.tsx
export const dynamicParams = false; // any other slug is a 404
export function generateStaticParams() {
return getAllPosts().map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = getPost(slug);
// ...render the post
}
One thing that tripped me up: in current versions of Next.js, params is a Promise and has to be awaited. Plenty of older tutorials show it as a plain object, so watch for that.
The result is plain HTML files sitting on a CDN, which is about as fast as a website can be. It reminded me of how much we fight for startup time in mobile apps. Here, the framework does most of that work for you.
Styling: Trading ThemeData for Tailwind
In Flutter I'd define a ThemeData and compose Padding, Container, and Row widgets. On this site I used Tailwind CSS, where styling is done with utility classes:
<a className="rounded-full bg-[#73f5c8] px-5 py-3 font-semibold text-[#07110d]">
View my work
</a>
My first reaction was "this is unreadable." After a day it felt similar to Flutter's widget composition: small, explicit building blocks applied right where they're used, rather than styles hidden in a separate file. For the shared parts of the theme, I keep the colours in CSS variables, which play the same role as a ColorScheme.
Responsive design was the other adjustment. Instead of LayoutBuilder or MediaQuery, Tailwind uses breakpoint prefixes: text-5xl md:text-7xl means "5xl on small screens, 7xl from the medium breakpoint up." It's the same idea, expressed inline.
Shipping: Deploys That Feel Like Magic
This was the biggest surprise. In mobile, a release involves builds, signing, store review, and waiting. With the project connected to Vercel, I push to GitHub and the site is live a minute later. Every branch also gets its own preview URL, which is like having a TestFlight build for every pull request.
After years of App Store review queues, that feedback loop is addictive.
What I'd Tell Another Mobile Developer
- Learn modern React first, briefly. Components, props,
useState, and JSX. A couple of hours is enough to start. Skip class components and older patterns. - Read the official docs for your exact version. Next.js changes quickly, and a lot of blog posts and Stack Overflow answers describe older behaviour. The
paramsPromise is a good example. - Default to Server Components. Reach for
"use client"only when you need interactivity or browser APIs. - Build something real. A portfolio is ideal: it's small, you care about it, and it covers routing, layouts, static generation, styling, and deployment.
- Lean on what you already know. Composition, unidirectional data flow, immutable state, and keeping rebuilds small all carry over from Flutter almost unchanged.
Wrapping Up
Learning Next.js didn't replace Flutter for me. Flutter is still my tool of choice for mobile apps. But it gave me a second way to think about where code runs, how pages get to users, and how fast shipping can be.
If you're a mobile developer curious about the web, building your own site is a great way to start. You'll learn the framework, and you'll end up with a portfolio to show for it.
Thanks for reading! If you're making the same jump from mobile to web, I'd love to hear how it's going.
Written by
Niluka Bandara
Lead Mobile Developer in Stockholm, building Flutter apps for fintech and payments.