技術1 閱讀

MVP 不是將就,是一種更聰明的開發順序

作為工程師,我們天生喜歡「做完整」:設計優雅的架構、抽象通用元件、配齊權限驗證、CI/CD、一套完善的設計系統……結果上線時間一拖再拖。

MVP(Minimum Viable Product,最小可行產品)這套思維,本質上是幫你用更小的代價驗證一個想法值不值得繼續投入

對開發者來說,它不是「寫爛一點的程式碼」,而是「在正確的時間做剛剛好的工程決策」。

先說清楚:什麼是 MVP?

比較正式一點的定義是:MVP 是一個功能最小、但足以交付給真實用戶使用,並用來收集回饋、驗證假設的產品版本。

這裡有三個關鍵資訊:

簡化成一句話:用最小的一堆程式碼,弄清楚一件事——這個東西有沒有人真正想用。

MVP 開發的核心思維

站在工程視角,可以把 MVP 思維拆成幾步:

  1. 明確要驗證的「一個問題」

    例如:「用戶是否願意用一個極簡線上記事工具,而不是繼續用微信發給自己、發郵箱。」

  2. 只保留驗證這個問題必須的功能

    建立筆記、列表展示、刪除一條——夠了。

  3. 用你最快的路徑實現

    不一定是「最潮技術棧」,而是你最熟的那一套。

  4. 在最小範圍上線,觀察真實使用

    先丟給身邊的同事、朋友,再考慮公開發布。

  5. 基於行為數據迭代,而不是基於腦補加需求

    看大家是否會回來繼續用,而不是聽他們說「不錯不錯」。

一個簡單對比:工程師常見兩條路線

非 MVP 路線(也就是我們經常走的那條):

MVP 路線

架構可以後面補回來,但要是沒人願意用,你就少搭了一整座「空城」。

實戰:用 Next.js 做一個記事類 MVP

下面用一個「線上筆記/備忘錄」作為範例,用 Next.js(App Router)走完一次 MVP 思路。

1. 先定義 MVP 範圍

需求刻意壓到很小:

暫時不做的東西:

這樣一個範圍,對任何熟悉 React 的開發者來說,1–2 天就可以搞出一個可用版本。

2. 專案腳手架與基本結構

用官方文件推薦方式建立 App Router 專案。

在終端機中:

bash
npx create-next-app@latest note-mvp
# 或者選擇 TypeScript、App Router 等預設選項
cd note-mvp
npm run dev

目錄結構中,app/ 目錄是核心。 [nextjs](https://nextjs.org/docs/app)

我們可以這樣安排最小結構:

3. 用 App Router 寫一個極簡 API

app/api/notes/route.ts 中,先用記憶體陣列模擬儲存(真實環境你可以換成 SQLite / Supabase 等)。

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 })
}

這段程式碼有很多「可以優化」的點,比如:

但在 MVP 階段,它已經滿足了「驗證:有人願意寫點什麼並回來看」的目標。

4. 寫一個簡單的頁面

app/page.tsx 中,用 Client Component 處理互動。

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>
  )
}

這個頁面非常「樸素」:一個 textarea,一個按鈕,一個列表。

但它已經完整打通了一個閉環:

從 MVP 角度看,這已經足以拿給 5–10 個測試用戶去試用,看看他們的真實行為。

這個 Next.js MVP 範例體現了哪些 MVP 思維?

圍繞剛才這段實現,我們可以反過來看:

沒有額外引入狀態管理庫、UI 框架、複雜後端服務。

雖然現在用的是記憶體儲存,但 API 路徑已經固定,將來換成資料庫只要改 route 裡的實現即可。

「有人會願意在瀏覽器裡寫一條簡短筆記,並在同一入口回來查看/刪除嗎?」

以一個熟悉 Next.js 的工程師水準,這個 MVP 級實現一天內可以完成並部署到 Vercel。

從這個起點開始,你可以按用戶行為來決定後續路線,例如:

常見誤區:把「工程完美」當成目標

在實際專案中,很多工程師會把這類 MVP 當成「臨時玩具」,心裡過不去:

這裡有兩個現實考慮:

MVP 並不是否定工程品質,而是把「高品質工程」放在需求被證明有效之後再做。

寫在最後:給工程師的 MVP 心態

可以把 MVP 思維理解成一種工程風險管理

對有經驗的開發者來說,一個實用的做法是:

SHARE

分享

分享這篇文章。