はじめに
「AIがコード書いてくれるなら、もうプログラミング学ばなくてよくない?」
2026年の今、新人エンジニアがまず直面するのはこの問いかもしれない。結論から言うと、AIを使いこなすためにこそ基礎力が必要だ。
筆者は個人開発プロジェクト「NBA ISO FLOW」(NBAトレード速報サイト)を、Claude Codeをペアプロ相手にしながらRust × Kotlin Compose for Webで構築した。その過程で得た「新人時代に知りたかった技術習慣」を7つ共有する。
📌 この記事で扱うこと:
- AIツール(Claude Code)との実践的な付き合い方
- 個人開発の実コードから学ぶ、現場で通用するエンジニアリング習慣
- 新人が「最初の一歩」で差をつけるための考え方
環境
| 項目 | バージョン/ツール |
|---|---|
| Rust | 2021 edition |
| axum | 0.8 |
| async-graphql | 7.0 |
| Kotlin | 1.9.22 |
| Compose for Web | 1.5.12 |
| AIツール | Claude Code (Opus 4.6) |
1. AIは「答え」じゃなく「壁打ち相手」として使え
新人がAIツールで最もやりがちなミスは、出力をそのままコピペすることだ。
Claude Codeと開発していて気づいたのは、AIが真価を発揮するのは「対話」のとき。例えば:
- ❌ 「RSSパーサー書いて」→ コピペ → 動かない → 困る
- ✅ 「RSSフィードを複数取得するとき、1つが失敗しても他を継続させたい。どういうエラーハンドリングが良い?」→ 設計を議論 → 自分で実装
実際のNBA ISO FLOWのRSSパーサーでは、こういうパターンに落ち着いた:
pub async fn fetch_all_feeds(&self) -> Result<Vec<NewsItem>> {
let mut all_news = Vec::new();
for (url, source) in RSS_FEEDS {
match self.fetch_feed(&RssFeed::new(url, source.clone())).await {
Ok(mut news) => {
info!("Fetched {} items from {}", news.len(), source);
all_news.append(&mut news);
}
Err(e) => {
error!("Failed to fetch feed from {}: {}", source, e);
// 1つ失敗しても残りは継続
}
}
}
all_news.sort_by(|a, b| b.published_at.cmp(&a.published_at));
Ok(all_news)
}
ポイント: matchで個別のフィードのエラーを捕捉し、ループ全体は止めない。この「部分的成功」の考え方は、マイクロサービスでもバッチ処理でも頻出する。
AIに「書いて」と頼む前に、「この設計どう思う?」と聞く習慣をつけよう。
2. ログに機密情報を出すな(1年目で一番やらかすやつ)
本番環境でprintln!デバッグしてDB接続文字列がログに流れた——新人がやらかしがちな事故No.1だ。
NBA ISO FLOWでは接続文字列をマスクする関数を最初から入れている:
/// 接続文字列をマスク(セキュリティのため)
fn mask_connection_string(url: &str) -> String {
if let Some(at_pos) = url.rfind('@') {
if let Some(scheme_end) = url.find("://") {
let scheme = &url[..scheme_end + 3];
let host_part = &url[at_pos..];
return format!("{scheme}****{host_part}");
}
}
"****".to_string()
}
// postgres://user:password@host:5432/db → postgres://****@host:5432/db
たった十数行で、ログに流れても安全な形に変換できる。
教訓: ログ出力のコードを書くとき、常に「これ本番で流れて大丈夫?」と自問する癖をつけよう。Claude Codeにコードレビューを頼むときも、「機密情報がログに出てないかチェックして」と明示的に指示すると精度が上がる。
3. エラーメッセージは「ユーザーの言葉」で書け
開発者がよくやるのは、エラーメッセージにスタックトレースや技術用語をそのまま出すこと。ユーザーはECONNREFUSEDと言われても何もできない。
フロントエンドでは、技術的なエラーをユーザーが理解できるメッセージに変換している:
fun buildNewsFetchErrorMessage(error: Throwable): String {
val message = error.message.orEmpty()
return when {
message.contains("Backend unavailable", ignoreCase = true) ->
"バックエンドが一時的に応答していません。しばらくしてから再試行してください。"
message.contains("timeout", ignoreCase = true) ->
"通信がタイムアウトしました。ネットワーク状態を確認して再試行してください。"
else ->
"ニュースの取得に失敗しました。時間をおいて再試行してください。"
}
}
ポイント: エラーメッセージには必ず**「次に何をすべきか」**を含める。「失敗しました」だけでは不親切。「〜してから再試行してください」まで書くのがプロの仕事だ。
4. テストは「異常系」から書け
テストを書くとき、まず正常系(ハッピーパス)を書きたくなる。でも実際にバグが潜むのは異常系だ。
NBA ISO FLOWのフロントエンドテストでは、正常系より異常系のテストの方が多い:
@Test
fun testFetchNewsItemsThrowsOnGraphQLError() = runTest {
val mockEngine = MockEngine {
val responseData = GraphQLResponse<TradeNewsData>(
data = null,
errors = listOf(GraphQLError("Backend unavailable"))
)
respond(
content = ByteReadChannel(Json.encodeToString(responseData)),
status = HttpStatusCode.OK, // HTTP的には200でもGraphQLエラー
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
val error = assertFailsWith<GraphQLClientException> {
client.fetchNewsItems()
}
assertTrue(error.message!!.contains("Backend unavailable"))
}
注目してほしいのは、HTTPステータスは200なのにGraphQLレイヤーではエラーというケース。こういう「一見成功に見える失敗」こそ本番で事故を起こす。
新人へのアドバイス: テストを書くときは「このコードはどう壊れうるか?」から考えよう。AIにも「このコードのエッジケースを5つ挙げて」と聞くと、見落としていたケースが見つかる。
5. 環境差異は「設計」で吸収しろ
「ローカルでは動いたのに本番で動かない」——新人が100%経験する通過儀礼だ。
NBA ISO FLOWでは、環境差異をコンパイル時に吸収する設計を採用している:
// 本番: 特定ドメインのみ許可
#[cfg(not(debug_assertions))]
let cors = CorsLayer::new()
.allow_origin(AllowOrigin::list([
"https://nba-iso-flow.com".parse().unwrap(),
]));
// 開発: 全オリジン許可
#[cfg(debug_assertions)]
let cors = CorsLayer::new()
.allow_origin(tower_http::cors::Any);
Rustの#[cfg]アトリビュートで、デバッグビルドとリリースビルドで挙動を切り替える。環境変数の読み忘れや設定ファイルの置き忘れといった人為的ミスが構造的に起こらない。
フロントエンド側も同様に、3段階のフォールバックで環境を自動検知する:
val apiEndpoint: String
get() {
// 1. グローバル変数(デプロイ時に注入)
val globalEndpoint = js("...") as String?
if (!globalEndpoint.isNullOrEmpty()) return globalEndpoint
// 2. ホスト名から自動判定
return when (window.location.hostname) {
"nba-iso-flow.com" -> "https://nba-iso-flow.com/graphql"
"localhost" -> "/graphql"
else -> "/graphql"
}
}
教訓: 環境差異は「気をつける」ではなく「仕組みで防ぐ」。設定ファイルやif文で分岐するより、コンパイル時ディスパッチや自動検知で解決する方が圧倒的に安全だ。
6. CIは「最初のコミット」から入れろ
「テスト書いたらCI入れよう」——これは永遠に来ない。最初のコミットからCIを設定しよう。
NBA ISO FLOWのCIは、PRの種類に応じて実行するチェックを自動で切り替える:
# PRの種類を自動判定
- name: Determine PR type
id: pr-type
run: |
TITLE="${{ github.event.pull_request.title }}"
if [[ "$TITLE" =~ ^docs ]]; then
echo "type=docs" >> $GITHUB_OUTPUT
elif [[ "$TITLE" =~ ^ci ]]; then
echo "type=ci" >> $GITHUB_OUTPUT
else
echo "type=code" >> $GITHUB_OUTPUT
fi
ドキュメントだけの変更でテストを回すのは無駄。でもコード変更ではフォーマット → Lint → テスト → カバレッジを全部回す。賢いCIは開発速度を落とさずに品質を担保する。
GitHub Actionsは無料枠でも十分使える。個人開発でも最初からCIを入れておくと、「壊れたコードをmainに入れてしまった」という事故を防げる。
7. 「UIの4状態」を意識しろ
フロントエンド開発で新人がよくやるのは、「データが取れた時」のUIしか作らないこと。でも実際には最低4つの状態がある:
| 状態 | 表示すべきもの |
|---|---|
| ローディング中 | スピナーやスケルトン |
| エラー | エラーメッセージ + リトライボタン |
| データなし | 空状態のメッセージ |
| データあり | コンテンツ本体 |
NBA ISO FLOWのCompose for Webでは、この4状態を明示的にハンドリングしている:
@Composable
fun App(apiClient: TradeNewsApiClient? = null) {
var backendStatus by remember {
mutableStateOf(BackendStatusSnapshot(BackendStatusState.CHECKING))
}
var latestNewsError by remember { mutableStateOf<Throwable?>(null) }
LaunchedEffect(healthCheckNonce) {
backendStatus = BackendStatusSnapshot(BackendStatusState.CHECKING)
backendStatus = try {
buildBackendStatusSnapshot(client.fetchHealthStatus())
} catch (e: CancellationException) {
throw e // コルーチンのキャンセルは再スロー必須!
} catch (e: Exception) {
BackendStatusSnapshot(
state = BackendStatusState.UNAVAILABLE,
detail = buildBackendUnavailableMessage(e)
)
}
}
// ...
}
特に注意してほしいのは CancellationException の再スロー。Kotlinのコルーチンでは、キャンセル例外を飲み込むとコルーチンが正常終了したとみなされ、リソースリークの原因になる。これはKotlin公式ドキュメントにも書いてあるが、実際にハマるまで気づかない人が多い。
ハマったポイント:Claude Codeとの付き合い方で学んだこと
ここまで7つの技術習慣を紹介したが、すべてに共通するのは**「AIに任せきりにしない」**ということだ。
Claude Codeで開発していて最もハマったのは、AIの出力を鵜呑みにして、自分で設計を考えなかった場面だった。例えば:
- AIが提案したエラーハンドリングが「全部
unwrap()で握りつぶす」パターンだった → 本番でサイレント障害発生 - AIが生成したテストが正常系だけで、異常系がゼロだった → マージ後にバグ発覚
逆にうまくいったのは:
- 「このRSSパーサー、1フィードが死んだ場合どうする?」と設計レベルで相談した → 部分的成功パターンの提案が返ってきた
- 「CORSの設定、dev/prodで分けたいけどどの方法がいい?」と聞いた →
#[cfg]ディスパッチの提案が来て、環境変数より安全な方法を採用できた
AIツールは「優秀だけど経験の浅いペアプロ相手」だと思うといい。 指示が曖昧だと曖昧な答えが返る。具体的な制約や意図を伝えれば、それに応じた提案が返ってくる。
まとめ:AI時代こそ「基礎体力」がモノを言う
| # | 習慣 | 一言まとめ |
|---|---|---|
| 1 | AIは壁打ち相手 | コピペではなく対話で使え |
| 2 | ログの機密管理 | 本番で流れて大丈夫か常に自問 |
| 3 | エラーメッセージ | ユーザーに次のアクションを示せ |
| 4 | テストは異常系から | 「どう壊れるか」を先に考えろ |
| 5 | 環境差異は設計で | 「気をつける」ではなく仕組みで防げ |
| 6 | CIは初日から | 品質ゲートは後回しにするな |
| 7 | UIの4状態 | ハッピーパスだけ作るな |
Claude Codeのようなツールは強力だが、「何が良いコードか」を判断できなければ、AIの出力を評価できない。基礎を身につけた上でAIを活用すれば、1年目からベテランに匹敵するアウトプットが出せる。
この記事で紹介したコードはすべて NBA ISO FLOW で実際に動いている。NBAのトレード情報・FA情報をリアルタイムで日本語配信しているので、NBA好きの方はぜひチェックしてみてほしい。Rust × Kotlin Compose for Web という技術スタックに興味がある方の参考にもなるはずだ。
新人エンジニアの皆さん、一緒に良いコードを書いていこう。
🏀 NBA ISO FLOW — NBAトレード速報サイト: https://www.nba-iso-flow.com/