Tech1 views

MVP Isn't About Settling — It's a Smarter Development Sequence

As engineers, we naturally like to “do it all”: design elegant architectures, abstract common components, set up authentication, CI/CD, a complete design system… and as a result, launch time keeps getting delayed.

The MVP (Minimum Viable Product) mindset is essentially about helping you verify whether an idea is worth pursuing at a lower cost.

For developers, it’s not about “writing crappy code,” but about “making the right engineering decision at the right time.”

First, let’s clarify: What is MVP?

A more formal definition: MVP is a product version with the smallest set of features, yet sufficient to deliver to real users and to collect feedback and validate hypotheses.

There are three key points:

Simplified into one sentence: Use the smallest amount of code to confirm one thing—whether anyone actually wants to use this thing.

Core Mindset of MVP Development

From an engineering perspective, the MVP mindset can be broken down into several steps:

  1. Clarify the “one question” to be validated

    For example: “Are users willing to use a minimal online note-taking tool instead of continuing to send messages to themselves on WeChat or via email?”

  2. Only keep the features necessary to validate this question

    Create a note, display a list, delete one—that’s enough.

  3. Use your fastest path to implement

    It doesn’t have to be the “trendiest tech stack,” but the one you know best.

  4. Launch in the smallest scope, observe real usage

    First throw it to colleagues and friends around you, then consider public release.

  5. Iterate based on behavior data, not on imagined requirements

    See if people come back to use it again, not by listening to them say “It’s good, it’s good.”

A Simple Comparison: Two Common Paths for Engineers

Non-MVP Path (the one we often take):

MVP Path:

Architecture can be added later, but if no one wants to use it, you’ve saved yourself from building an entire “empty city.”

Hands-on: Building a Note-taking MVP with Next.js

Below, we use an “online note/memo” as an example, going through the MVP approach with Next.js (App Router).

1. First, define the MVP scope

Intentionally keep the requirements very small:

Things not done for now:

With such a scope, any developer familiar with React can produce a usable version in 1–2 days.

2. Project scaffold and basic structure

Create an App Router project using the official documentation recommended method.

In the terminal:

bash
npx create-next-app@latest note-mvp
# 或者选择 TypeScript、App Router 等默认选项
cd note-mvp
npm run dev

In the directory structure, the app/ directory is core. [nextjs](https://nextjs.org/docs/app)

We can arrange the minimal structure like this:

3. Write a minimal API with App Router

In app/api/notes/route.ts, first use an in-memory array to simulate storage (in a real environment, you can switch to SQLite / Supabase, etc.).

ts
// app/api/notes/route.ts
import { NextResponse } from 'next/server'

type Note = {
  id: string
  content: string
  createdAt: string
}

let notes: Note[] = []

export async function GET() {
  return NextResponse.json(notes)
}

export async function POST(request: Request) {
  const { content } = await request.json()
  if (!content || typeof content !== 'string') {
    return new NextResponse('Invalid content', { status: 400 })
  }

  const note: Note = {
    id: crypto.randomUUID(),
    content,
    createdAt: new Date().toISOString(),
  }

  notes.unshift(note)
  return NextResponse.json(note, { status: 201 })
}

export async function DELETE(request: Request) {
  const { searchParams } = new URL(request.url)
  const id = searchParams.get('id')
  if (!id) {
    return new NextResponse('Missing id', { status: 400 })
  }

  notes = notes.filter((n) => n.id !== id)
  return new NextResponse(null, { status: 204 })
}

This code has many “could be optimized” points, such as:

But in the MVP stage, it already meets the goal of “validating: someone is willing to write something and come back to see it.”

4. Write a simple page

In app/page.tsx, use a Client Component to handle interactions.

tsx
'use client'

import { useEffect, useState } from 'react'

type Note = {
  id: string
  content: string
  createdAt: string
}

export default function Home() {
  const [notes, setNotes] = useState<Note[]>([])
  const [input, setInput] = useState('')
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    fetch('/api/notes')
      .then((res) => res.json())
      .then((data) => setNotes(data))
  }, [])

  async function handleAdd(e: React.FormEvent) {
    e.preventDefault()
    if (!input.trim()) return

    setLoading(true)
    try {
      const res = await fetch('/api/notes', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: input.trim() }),
      })
      const note: Note = await res.json()
      setNotes((prev) => [note, ...prev])
      setInput('')
    } finally {
      setLoading(false)
    }
  }

  async function handleDelete(id: string) {
    await fetch(`/api/notes?id=${id}`, { method: 'DELETE' })
    setNotes((prev) => prev.filter((n) => n.id !== id))
  }

  return (
    <main style={{ maxWidth: 600, margin: '2rem auto', padding: '0 1rem' }}>
      <h1>Minimal Notes MVP</h1>

      <form onSubmit={handleAdd} style={{ marginBottom: '1rem' }}>
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          rows={3}
          style={{ width: '100%', marginBottom: '0.5rem' }}
          placeholder="写点什么..."
        />
        <button type="submit" disabled={loading}>
          {loading ? '保存中...' : '保存'}
        </button>
      </form>

      <section>
        {notes.length === 0 && <p>还没有任何笔记。</p>}
        {notes.map((note) => (
          <article
            key={note.id}
            style={{
              border: '1px solid #ddd',
              padding: '0.75rem',
              marginBottom: '0.75rem',
            }}
          >
            <p style={{ whiteSpace: 'pre-wrap' }}>{note.content}</p>
            <small style={{ color: '#666' }}>
              {new Date(note.createdAt).toLocaleString()}
            </small>
            <div>
              <button
                onClick={() => handleDelete(note.id)}
                style={{ marginTop: '0.25rem' }}
              >
                删除
              </button>
            </div>
          </article>
        ))}
      </section>
    </main>
  )
}

This page is very “plain”: a textarea, a button, and a list.

But it already completes a full loop:

From an MVP perspective, this is enough to give to 5–10 test users to try and see their real behavior.

What MVP thinking does this Next.js MVP example embody?

Looking back at this implementation, we can see:

No extra state management libraries, UI frameworks, or complex backend services introduced.

Although it uses in-memory storage now, the API path is fixed, and switching to a database in the future only requires changing the implementation in the route.

“Would someone be willing to write a short note in the browser and come back to the same place to view/delete it?”

For an engineer familiar with Next.js, this MVP-level implementation can be completed and deployed to Vercel within a day.

From this starting point, you can decide the next steps based on user behavior, for example:

Common Pitfall: Treating “Engineering Perfection” as the Goal

In real projects, many engineers regard this kind of MVP as a “temporary toy” and can’t get past it mentally:

Here are two practical considerations:

MVP does not deny engineering quality, but places “high-quality engineering” after the need is proven valid.

Final Thoughts: MVP Mindset for Engineers

You can think of the MVP mindset as a kind of engineering risk management:

For experienced developers, a practical approach is:

SHARE

Share

Share this article.