「実力がついたら、ちゃんとしたものを作って応募しよう」——未経験からエンジニアを目指す人と話していると、本当によく聞く言葉です。
でも、ひとつ確認させてください。その"実力"、応募先の誰が、どうやって確認するんでしょうか? 採用担当が見られるのは、あなたの頭の中の理解度ではなく、"公開されたURLとコード"だけです。だから「準備」をどれだけ積んでも、外から見える形になっていなければ、評価のしようがありません。
この記事は、その「準備の沼」から抜けるための、技術的にいちばん現実的な方法を書きます。テーマは 「最初のポートフォリオに何を作るべきか」と「それを"見せられる最小の実績"にする技術構成」。完璧な大作ではなく、認証付きの小さなアプリを1個、公開URLまで持っていくことをゴールにします。
※ モチベーションや「やる気の出し方」の話ではありません。作ったものを就活・案件という出口にどう繋げるか、という採用側の目線から逆算した技術記事です。
なぜ「動くものを1個公開」が効くのか(採用側の目線)
未経験の応募書類はどうしても横並びになります。「Progateを一通り」「Udemyを3本」「基礎は理解しています」——これらは全員が書くので差がつきません。一方で、こう書いてある人は一気に解像度が上がります。
「ログイン機能付きのタスク管理アプリを作って公開しました。URL: https://... / ソース: https://github.com/...」
採用側がこの1行から読み取れることは、想像以上に多いです。
- 最後まで作り切れる(途中で投げ出さない)
- 認証・DB・デプロイという"つなぎ目"を自分で繋いだ経験がある
- 公開する=他人に見られる状態にする度胸がある
- GitHubを見ればコードの書き方・コミットの粒度まで分かる
つまりポートフォリオは「すごい技術」を見せる場ではなく、「この人は仕事を任せても完了させられそうか」を判断させる材料です。だから規模より、認証 → データ操作 → 公開というエンジニアリングの一周を、小さくても自分で通したかが効きます。
何を作るか:選ぶ基準は3つだけ
ネタ探しで止まる人が多いので、基準を絞ります。アイデアの良し悪しで悩まないでください。
- 自分が毎日使える題材にする — タスク管理、家計簿、読書ログ、献立メモ。継続して触るので「動かない放置アプリ」になりにくい。
-
機能は1つに絞る — 「あれもこれも」は完成しません。
1ユーザーが、1種類のデータを、追加・一覧・更新・削除できる。これで十分です。 - 認証付きにする — ここが最重要。ログインを入れると「自分のデータだけ見える」を実装することになり、実務でいちばん問われる"データの分離"の理解を自然に示せます。
この記事では例として 「ログインできるタスク管理アプリ」 を作ります。地味に見えますが、地味なものをちゃんと公開まで持っていける人が、現場では一番信頼されます。
最小構成:ディレクトリと技術スタック
スタックは Next.js(App Router)+ Supabase(認証・DB)+ Vercel(公開)。フロント・認証・DB・ホスティングが少ない設定で繋がり、未経験が「一周」を通すのに向いています。ディレクトリは欲張らず、これくらいで成立します。
my-task-app/
├── app/
│ ├── login/{ page.tsx, actions.ts } # ログイン画面・サインイン処理
│ ├── tasks/{ page.tsx, actions.ts } # 一覧(Server Component)・CRUD(Server Actions)
│ └── layout.tsx
├── lib/supabase/{ server.ts, client.ts } # サーバー用 / ブラウザ用クライアント
├── supabase/schema.sql # テーブル定義 + RLS
├── .env.local # キー(gitに上げない)
├── README.md # ← 採用者が最初に読む
└── package.json
1. データベース:1テーブル + RLS
Supabase の SQL エディタで、テーブルと RLS(Row Level Security) を一緒に書きます。RLS は「自分の行だけ触れる」をDB側で保証する仕組みで、ここを入れておくだけでポートフォリオの説得力が変わります。
-- supabase/schema.sql
create table tasks (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) default auth.uid(),
title text not null,
done boolean not null default false,
created_at timestamptz not null default now()
);
alter table tasks enable row level security;
-- 「自分の user_id の行」だけ読み書きできるポリシー
create policy "own tasks only" on tasks
for all
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
ポイントは user_id ... default auth.uid() と RLS の using / with check。これで、たとえアプリ側のコードにバグがあっても、他人のタスクは構造的に見えません。「セキュリティ意識ある?」への無言の回答になります(RLSの"なぜ危険か"の詳細は別記事に譲り、ここでは採用者に伝わる説得力の一点に絞ります)。
2. 認証:ログインを Server Action で
App Router では「サーバー用」「ブラウザ用」でクライアントを分けるのが定石です。@supabase/ssr の createServerClient を cookies() 付きでラップした lib/supabase/server.ts を1個用意しておきます(公式ドキュメントのコピペでOK)。それを使ってログインを書きます。最小なのでメール+パスワードにします。
// app/login/actions.ts
'use server'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
export async function login(formData: FormData) {
const supabase = await createClient()
const { error } = await supabase.auth.signInWithPassword({
email: String(formData.get('email')),
password: String(formData.get('password')),
})
if (error) redirect(`/login?error=${encodeURIComponent(error.message)}`)
redirect('/tasks')
}
フォーム側(app/login/page.tsx)は <form action={login}> の中に email / password の <input> を置くだけです。
3. CRUD:一覧(Read)と追加・更新(Create / Update)
一覧は Server Component で直接 DB を読みます。未ログインなら /login へ飛ばす——このガードを入れておくのも地味に評価されます。
// app/tasks/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { addTask } from './actions'
export default async function TasksPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login') // ← 未ログインは弾く
const { data: tasks } = await supabase
.from('tasks')
.select('id, title, done')
.order('created_at', { ascending: false })
return (
<main>
<form action={addTask}>
<input name="title" placeholder="やること" required />
<button type="submit">追加</button>
</form>
<ul>
{tasks?.map((t) => (
<li key={t.id}>{t.done ? '完了 ' : '未 '}{t.title}</li>
))}
</ul>
</main>
)
}
追加・更新・削除は Server Actions に切り出します。revalidatePath で再描画されるので、状態管理ライブラリは不要です。
// app/tasks/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
export async function addTask(formData: FormData) {
const title = String(formData.get('title') ?? '').trim()
if (!title) return
const supabase = await createClient()
// user_id は DB 側の default auth.uid() が入れてくれる
const { error } = await supabase.from('tasks').insert({ title })
if (error) throw new Error(error.message)
revalidatePath('/tasks')
}
export async function toggleTask(id: string, done: boolean) {
const supabase = await createClient()
await supabase.from('tasks').update({ done: !done }).eq('id', id)
revalidatePath('/tasks')
}
export async function deleteTask(id: string) {
const supabase = await createClient()
await supabase.from('tasks').delete().eq('id', id)
revalidatePath('/tasks')
}
これで C/R/U/D が一周しました。コード量はこの程度です。「小さい」ことは欠点ではなく、最後まで通せる強みです。
4. README:採用者が最初に読む“顔”
ここを飛ばす人が本当に多いのですが、ポートフォリオの価値はREADMEで半分決まります。 採用側はまずREADMEを開き、3分で「何ができる人か」を判断します。最低限これを書いてください。
# タスク管理アプリ
ログインして、自分のタスクだけを追加・完了・削除できる最小Webアプリ。
## デモ
- 公開URL: https://my-task-app.vercel.app
- テスト用: demo@example.com / demo1234
## 使った技術と、選んだ理由
- Next.js (App Router) … サーバー側でデータを取得して表示したかった
- Supabase … 認証とDBをまとめて用意でき、RLSで「自分のデータだけ」を保証できる
- Vercel … GitHub連携で push するだけで自動デプロイされる
## 工夫した点 / 既知の課題
- RLSで「他人のタスクはDBレベルで見えない」状態にした
- バリデーションとテストはこれから追加する
特に効くのが 「技術を選んだ理由」 と 「既知の課題」 です。理由が書ける=判断ができる、課題が書ける=自分のコードを客観視できる。完璧を装うより、できていない所を正直に言語化できる人のほうが現場では信頼されます。
5. Vercel で公開:ここまでやって初めて「実績」
ローカルで動くだけでは、まだ「準備」のままです。URLになって初めて見せられます。 手順は、GitHubにpush → Vercelでそのリポジトリを import → 環境変数 NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY を登録 → Deploy → Supabaseの「Authentication → URL Configuration」に発行されたURLをRedirect先として追加、の流れです。
⚠️ .env.local を .gitignore に入れ忘れてキーをコミットする事故が定番なので、push前に必ず確認してください。これで「URLを送れば誰でも触れるアプリ」になります。ここがゴールラインです。
AIと一緒に作ると、何が変わるか
ここまでのコードは、ゼロから一人で書こうとすると未経験には重いです。でもAIと協働すれば、この一周を現実的な時間で通せます。 そして大事なのは、AI協働そのものが今いちばん評価されるスキルになっている点です。「コードを書く力」と「AIに的確に指示して、出力を検証する力」が同時に身につく——これが今の時代の最短ルートだと考えています。
たとえば Claude Code にはこう頼みます。具体的な制約を渡すのがコツです。
tasks テーブルへの CRUD を Next.js の Server Actions で作ってください。
制約:
- DB は Supabase、クライアントは @supabase/ssr のサーバー用を使う
- RLS 前提で、user_id はクライアントから渡さず DB の default auth.uid() に任せる
- 追加・完了トグル・削除を作り、各操作後に revalidatePath('/tasks') する
- 型は TypeScript、any は使わない
そして**出てきたコードを鵜呑みにしないための「レビュー依頼」**も投げます。ここを自分でやれるかが、AI時代に「使われる人」か「使いこなす人」かの分岐点です。
このコードを、未認証ユーザーが他人のタスクを見たり消したりできる穴がないか、
RLS と auth.getUser() の観点でレビューしてください。
危険な箇所は「なぜ危険か」と「最小の直し方」をセットで教えてください。
「AIに丸投げして動いた」で終わらせず、RLSの観点で自分でレビューを依頼できる——この一連の動きこそ、ポートフォリオのコードと一緒に見せたい"実力"です。
まとめ:「実力がついてから」ではなく「1個公開してから」
もう一度だけ。「実力がついてから応募しよう」は、私の体感ではだいたい来ません。 来ないというより、実力は"作って公開する過程"でしか本当には育たないからです。
- 何を作るか:自分が使える題材を1つ、機能は1つ、ただし認証付きで
- 技術構成:Next.js + Supabase(RLS)+ Vercel の最小一周
- 見せ方:READMEに「技術を選んだ理由」と「既知の課題」を書く
- 公開:URLになって初めて「実績」になる
- AI協働:作るのも、レビューも、AIと一緒に。その過程自体がスキルになる
完璧な大作を1年かけて準備するより、小さくても動くものを1個、今週URLにする。 その1個が、横並びの応募書類から抜け出す最初の足場になります。準備の沼から、一歩だけ出てみてください。
未経験者向けの講座を運営しています
未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を、全20セッションで体系化した教材です。この記事のような「認証付きの最小アプリを公開まで持っていく」流れを、Claude Code(AI)と協働しながら一本通せる構成にしています。コードを書く力と、AIを使いこなす力が同時に身につくのが狙いです。
- 無料体験版(git clone してすぐ動く・最初の数セッション分・⭐ Star もよろしくお願いします)→ https://github.com/ayies128/next-ai-camp-trial
- 教材完全版+月5,500円のメンタリング(全20セッション+チャットで質問し放題)→ https://menta.work/plan/20251?ref=qiita
- YouTube『AIエンジニア情報局』(AI×開発ニュースを1本5分でキャッチアップできる別運営チャンネル・無料)→ https://www.youtube.com/channel/UC1rXVD9WYsQPQEWZyd-A1KA/?ref=qiita
※ Qiita 読者の方には易しすぎる内容です。初心者の知り合いへの紹介や、新人研修・後輩育成の参考として使ってもらえたら嬉しいです。