Harinezumi_AI_Hackathonに行ってきました!
どうもこんにちは、YAMAです。
Harinezumi_AI_Hackathonに行ってきました!
今回が初めての外部ハッカソンということで緊張したのですが、企業賞のTechTrain賞をとってきました!
今回のハッカソンは一体どういうものか?
今回行ってきたハッカソンはAIを使ったプロダクトを作るハッカソンで、学生団体のTech.Uniさんが主催しているイベントです!
正式名称は「Harinezumi AI Hack」で、キャッチコピーは「1人前のハリネズミ(エンジニア)へ進化しよう!」でした。
ハッカソン = Hack × マラソンということで、一週間かけて開発していきます。
ざっくりとした概要はこんな感じです。
| 項目 | 内容 |
|---|---|
| 開催日 | 2026/9/20(日)・9/27(日)の2日間 |
| 会場 | 1日目:株式会社ストライク / 2日目:サイボウズ株式会社 |
| 主催 | Tech.Uni |
| 開発テーマ | AI |
| 発表時間 | 5分/チーム |
1日目にアイデア出しから開発をスタートして、2日目に成果発表をするという流れでした。
主催のTech.Uniってどんな団体?
Tech.Uniさんは、全国から集まった学生同士が出会える場所をコンセプトにした学生エンジニア団体です。
2020年10月26日に設立されて、メンバーは約200名もいるそうです。
関西学院大学の社会連携センターが広報支援をしていて、顧問や技術アドバイザーにも関西学院大学の先生がついています。
MVVはこんな感じでした。
- Mission:楽しみ,つながり,築く
- Vision:テクノロジーを用いて新たなサービスを作りたい積極性のあるメンバーが、十二分に力を発揮できる組織を作る
- Value:Be innovate / Be active / Be responsive
ハッカソン以外にも、いろいろなイベントを開催しています。
- AIディアソン2026:ChatGPTやv0を使って、アイデア出しからアプリ開発、成果発表までを1日で体験する1dayものづくりイベント
- AIネタナイト2026:watnowさん、PKSHA Technologiesさんと共同開催した、関西の学生エンジニアによる交流イベント
- サイボウズ勉強会:サイボウズの大阪オフィスを貸し切っての勉強会
- STORESオンライン勉強会:エンジニアを目指す女子学生向けのオンライン勉強会
他にも、メンバー向けにTechTrain、Udemy、Progate、paizaなどの学習ツールを使える独学支援や、オンライン勉強会、外部勉強会への参加といった学習支援もしているみたいです。
有料コンテンツを団体で購入しているものもあるらしく、学生にとってはかなりありがたい環境だと思います。
スポンサー
会場は2日間とも企業さんのオフィスをお借りしていました。
- 会場協賛:株式会社ストライク(9/20)、サイボウズ株式会社(9/27)
- 運営サポート・企業賞:株式会社TechBowl
- 企業賞:シナジーマーケティング株式会社
審査について
審査項目は以下の5つでした。
| 審査項目 | 見られるポイント |
|---|---|
| コンセプト | テーマとどれぐらいマッチしているか |
| 新規性 | 着眼点の新しさ、組み合わせの意外性、「その手があったか」と思わせるか |
| 完成度・技術力 | デモが実際に動くか、技術的な挑戦度、細部の作り込み |
| オモシロさ | 触ってみたいか、人に話したくなるか、体験としての引きの強さ |
| プレゼン | 構成の分かりやすさ、デモの見せ方、熱意 |
採点形式は、**オーディエンス形式(40点)と審査員形式(20点)**の2つに分かれていました。
- オーディエンス形式(40点):コンセプト・新規性・オモシロさ・プレゼン
- 審査員形式(20点):完成度・技術力
配点を見るとオーディエンス側の比重がかなり大きいので、技術力だけじゃなく「どれだけ面白いと思ってもらえるか」「どれだけ伝わるか」がめちゃくちゃ大事なハッカソンでした。
プレゼンは1チーム5分で、発表順は成果発表の開始時にランダムで公開される形式でした。
賞品
| 賞 | 賞品 |
|---|---|
| 最優秀賞 | Amazonギフト券 3万円 |
| 優秀賞 | Amazonギフト券 1人3千円 |
| シナジーマーケティング賞 | AirTag |
| TechTrain賞(受賞!) | Raspberry Pi × 面談チケット |
1日目の流れ…しかし…
| 時間 | 内容 |
|---|---|
| 10:00〜 | オープニング(団体紹介・スポンサー紹介・テーマ発表・審査方法の説明など) |
| 〜12:00 | アイスブレイク・アイデア出し |
| 12:00〜13:00 | ランチ休憩 |
| 13:00〜17:30 | アイデア出し・チーム開発 |
| 17:30〜 | 進捗発表・クロージング |
なのですが、自分は残念ながら仕事だったので、悲しいことに一日目に出席できませんでした。(悲しい…)
ですが、その代わりにほかのメンバーが出席してくれていたので、1日目のアイデア出しはメンバーに進めてもらいました。
作ったもの:LT会支援Webアプリ
ここからは、自分たちのチームが作ったものについて書いていきます。
作ったのは、LT会を気軽に立てて、見つけて、参加できるWebアプリです!
リポジトリはこちら → Me1td0wn76/HarinezumiAI_hack
発表スライドはこちら → LT会支援アプリ(Slidev)
テーマ「AI」を「出会い」と捉えた
テーマが「AI(アイ)」だったので、AIを「出会い(で”あい”)」と捉えて、LT会で人が出会えるアプリを作ることにしました。
LT会って、話したい人と聞きたい人が出会える場なんですけど、いざ開こうとすると3つの壁があります。
- 人が集まらない:告知しても届かず、いつもの顔ぶれで終わる
- 集まれる日がわからない:人が集まる日にしか開けないのに、みんなの都合が見えない
- 日程調整がめんどう:候補日を送って、返事を集めて、数えて……開く前に疲れてしまう
開くハードルが高いと、出会いの場そのものが生まれない、というのが出発点です。
日程調整を、出会いの入口にする
一番のアイデアは、日程を決める前にLT会を全国へ公開することです。
| 流れ | |
|---|---|
| いつものLT会 | 身内で日程を調整 → 日時を決めて告知 → 来られる人だけ集まる |
| このアプリ | 日程を決める前に全国へ公開 → 気になった人が ○△× で答える → いちばん集まれる日に開催・通知 |
日程調整の ○△× と、SNSのタイムラインやフォローを組み合わせることで、候補日への回答がそのまま「行きたい」のサインになるようにしました。
SNSに投稿するくらいの気軽さで、LT会を立てる・見つける・参加するができるのを目指しています。
アプリの流れ
1. 立てる(主催者)
タイトルと候補日を書くだけでLT会を作れます。入力するたびに、横の稲妻が充電されていきます。

2. 集まる(参加者)
主催者が共有URLを送ると、参加者はログインなしで ○△× を回答できます。

3. 決まる(主催者)
候補日ごとの人数を見て「この日に決定」を押すと、回答した人に通知が届きます。Discordに自動で投稿することもできます。

他にも、タグ・キーワード・開催形式(オンライン / オフライン)での検索、ユーザーのフォロー、公開プロフィール、登壇・聴講の参加表明、みんなのカレンダー、団体(サークル・研究室)ごとの管理などの機能があります。
触りたくなる仕掛け
審査項目に「オモシロさ」があったので、触ってみたくなる小さな仕掛けも入れました。
- 読み込み中は、ハリネズミが稲妻を充電して、満タンになると跳ねる
- 見出しのアイコンがカーソルから逃げる
- ○△× を選ぶと、その色の輪が広がる
- 動きが苦手な人はOFFにできて、OSの「視差効果を減らす」設定にも従う
技術スタック
| 分類 | 技術 |
|---|---|
| フロントエンド | Next.js 16 (App Router) / React / Tailwind CSS |
| バックエンド | NestJS 12 / Prisma 7 |
| データベース | PostgreSQL 17 |
| 認証 | メール + パスワード(bcrypt + JWT)、Googleログイン(OAuth 2.0 + PKCE) |
| 通知 | Discord Incoming Webhook |
| デプロイ | Vercel / Render / Neon |
| 発表スライド | Slidev(GitHub Pagesで公開) |
全体の構成はこんな感じです。
ブラウザ ⇄ Next.js(画面 + BFF) → NestJS(Controller → Service → Repository) → PostgreSQLリポジトリは pnpm workspace の monorepo にしていて、中身はこう分けています。
apps/
web/ Next.js(画面 + BFF)
api/ NestJS + Prisma(REST API)
packages/
shared/ APIのリクエスト/レスポンス型と列挙値(webとapiの両方からimport)誰でも使える公開サービスとして作ったので、守りの部分も入れています。
LT会の会場だと参加者全員が同じWi-Fi(同じIP)から一斉に回答するので、それでも詰まらないようにレート制限の上限を決めたり、通報・ブロック機能を入れたりしました。
数字で見るとこんな感じです。
| 項目 | 数 |
|---|---|
| 画面数 | 21 |
| テスト | 175(単体 161 + e2e 14、push ごとに CI で実行) |
| マージしたPR | 31 |
なぜこの構成にしたのか
TechTrain賞をいただけたのは、この構成の部分が大きかったと思っています。なので、ここはちょっと詳しく書きます。
設計の段階から、決めたことと理由を docs/design.md と docs/open-questions.md に残しながら進めていました。
フロントとバックを分けた理由:Webとスマホの両方に対応するため
このアプリは、まずWebアプリとして作って、将来的にはスマホアプリにも展開する前提で設計しました。
なので、LT会の作成・日程調整・フォローといった業務ルールとデータは全部NestJSのAPIに集めて、WebとスマホのどちらからでもAPIを使える形にしています。
その上で、Webの画面とNestJSの間には、Next.jsをBFF(Backend For Frontend)として挟んでいます。
Webブラウザ ⇄ Next.js(Web用のBFF) ─┐
├→ NestJS(共通のAPI) → PostgreSQL
スマホアプリ(今後) ────────────────┘Next.jsだけでも、Route HandlersでAPIを作ることはできます。
ただ、それだとスマホ向けのAPIがWebの画面のコードと同じ場所に混ざってしまい、Webの都合(Cookieや、画面ごとのデータの形など)がAPIに入り込みやすくなります。
NestJSを独立させて、Webの都合はBFFのNext.jsに閉じ込めることで、NestJSはどのクライアントからも使えるシンプルなAPIのままにできます。
スマホアプリを作るときも、NestJSのAPIはそのまま使えて、スマホ側の都合はスマホ側(必要ならスマホ用のBFF)で引き受ければいい、という形です。
TechBowlの方に褒めていただいたのも、まさにこの「スマホとWebの両方に対応するために、BFFを挟んでNestJSにつなげる」ところでした。
また、フロントとバックが分かれているので、画面を作る人とAPIを作る人が並行して開発しやすいのもメリットでした。
APIの形さえ決まっていれば、お互いを待たずに進められます。
その代わり、分けるとこんなコストがかかります。
- デプロイ先が2つになる
- CORSや、オリジンをまたいだ認証(Cookieやトークン)を自分で扱う必要がある
- フロントとバックで型定義が二重管理になりやすい
なので、このコストをそれぞれ潰す形で構成を決めました。
| 分けるコスト | 対策 |
|---|---|
| CORS・認証まわり | BFF構成にして、ブラウザはNext.jsとだけ通信する |
| 型の二重管理 | monorepoにして、型を packages/shared の1か所に置く |
| デプロイ先が2つ | Vercel(web)/ Render(api)/ Neon(DB)の無料枠で分担する |
BFF構成:ブラウザからNestJSを直接叩かない
ブラウザはNext.jsとだけ通信して、NestJSのAPIはNext.jsのサーバー側(Server Component / Server Function)からだけ呼ぶようにしました。
APIを呼ぶ処理は apiFetch という関数1つにまとめています。
// apps/web/src/lib/api.ts(抜粋)
import 'server-only';
export async function apiFetch<T>(path: string, init: ApiFetchInit = {}): Promise<T> {
// ...
if (auth) {
const token = (await cookies()).get(TOKEN_COOKIE)?.value;
if (token) headers.set('Authorization', `Bearer ${token}`);
}
const res = await fetch(`${API_URL}${path}`, { ...rest, headers /* ... */ });
// ...
}こうすると、
- ブラウザから見るとNext.jsしかいないので、CORSの設定がいらない
- JWTはNext.js側のhttpOnly Cookieに入れておけるので、ブラウザのJavaScriptからトークンが見えない
- NestJSはCookieを扱わず、
Authorizationヘッダーのトークン(Bearer)だけを見るので、スマホアプリからも同じAPIをそのまま使える import 'server-only'を付けているので、間違ってクライアント側から呼ぶとビルドで気づける
というメリットがあります。
上のコードで、Cookieからトークンを取り出して Authorization ヘッダーに付け替えている部分が、まさにBFFの仕事です。
monorepo + 共有パッケージ:型を1か所に
APIのリクエスト/レスポンスの型と列挙値は packages/shared に置いて、webとapiの両方からimportしています。
APIのレスポンスを変えたらweb側で型エラーになってすぐ気づけるので、4人で別々の機能を触っていても、フロントとバックの食い違いが起きにくくなります。
NestJSの中は Controller → Service → Repository の3層
NestJS側は、レイヤードアーキテクチャで3層に分けました。
| レイヤー | 役割 |
|---|---|
| Controller | HTTPリクエストを受け取って、レスポンスを返す |
| Service | 業務ルールを書く |
| Repository | Prismaを使ってDBにアクセスする |
例えば、フォロー機能のServiceはこんな感じです。
// apps/api/src/modules/follows/follows.service.ts(抜粋)
async follow(followerId: string, followingId: string): Promise<void> {
if (followerId === followingId) {
throw new BadRequestException('自分自身はフォローできません');
}
const target = await this.users.findById(followingId);
if (!target) {
throw new NotFoundException('ユーザーが見つかりません');
}
await this.follows.upsertFollow(followerId, followingId);
}「自分自身はフォローできない」「存在しないユーザーはフォローできない」といったルールはServiceに書いて、実際にDBに書き込む部分(Prismaの upsert)はRepositoryに任せています。
Controllerは受け取ったリクエストをServiceに渡すだけです。
実は、Repositoryを置くかどうかは設計を見直すときにも検討しました。
Prisma自体がDBアクセスを抽象化してくれているので、Repositoryを挟むと「Prismaを呼ぶだけのメソッド」が増えがちだからです。
それでもRepositoryを置いたのは、
- Serviceに
PrismaServiceを直接注入しないので、ServiceのユニットテストをDBなしで書ける - DBの型をそのまま返さず、
*.mapper.tsでpackages/sharedの型に変換してから返すので、メールアドレスのような本人以外に返してはいけない情報が外に出にくい - 機能ごとにNestJSのモジュール(
events、responses、follows、notificationsなど)を分けて、どこに何を書くかが全員同じになる
からです。
ルールを文書にして、人もAIも同じルールで書く
最後に、決めた構成を文書に残して守るようにしました。
- READMEに「APIを足すときは
apps/api/src/modules/<機能>/に module / controller / service / repository を追加して、レスポンス型はpackages/sharedに置く」のような開発の流れを書く - 設計で決めたことと理由は
docs/open-questions.mdに記録して、あとから蒸し返さない - Claude Code向けの
CLAUDE.mdにも「BFF構成」「APIの3層」「型はpackages/shared」を守る約束として書いて、AIにも同じルールでコードを書かせる
4人+AIで31個のPRを並行してマージしていくので、どこに何を書くかを全員で揃えておくことを大事にしました。
チーム開発
チームは4人で、担当はこんな感じでした。
| メンバー | 担当 |
|---|---|
| Me1td0wn76(YAMA) | 設計・API・デプロイ、検索・タグ・開催形式、共有・Discord通知、通報・レート制限 |
| Sabigon-MA | デザイン、登壇・聴講の参加表明、みんなのカレンダー、APIのテスト整備 |
| Tongari-Boy | ユーザーのフォロー、回答コメントの表示、決定前の確認ダイアログ |
| shouras | アプリ内通知、公開プロフィール、団体機能 |
機能ごとにGitHub Issueを分けて、それぞれがPRで持ち寄る形で開発しました。
開発にはClaude Codeを使っていて、NestJS・Prisma・Reactのベストプラクティスなどのスキルをリポジトリに入れて、AIと一緒に開発を進めました。
これから
今後は、フォロー中の人のLT会が流れてくるフィード、GitHub・Xログイン、スマホアプリなどを作っていく予定です。
結果:TechTrain賞をいただきました!
2日目の成果発表の結果、株式会社TechBowlさんの企業賞であるTechTrain賞をいただきました!
賞品はRaspberry Piと面談チケットです!
初めての外部ハッカソンで、賞をとることができました。
自分的には、賞をとれたきっかけは構成の部分だと思っています。
実際に、TechBowlの方からも、スマホとWebの両方に対応するためにBFFを挟んでNestJSにつなげる構成について、「わかってる作り方」と褒めていただきました。
上で書いたように、構成をなんとなくではなく理由を説明できる形で決めて、それをチーム全員(とAI)で守ったところを見てもらえたのかなと思います。
おまけ:BFFってなに?
記事の中で何度も出てきた「BFF」について、簡単にまとめておきます。
BFF = Backend For Frontend
BFFは Backend For Frontend の略で、名前の通り「特定のフロントエンドのためだけに用意するバックエンド」のことです。
(Best Friend Foreverではないです。)
普通の構成だと、Webもスマホも同じAPIを直接呼びます。
Webブラウザ ─┐
├→ API
スマホアプリ ─┘BFFを使う構成では、クライアントごとに専用の窓口(BFF)を置いて、その後ろにある共通のAPIを呼びます。
Webブラウザ → Web用BFF ─┐
├→ 共通のAPI
スマホアプリ → スマホ用BFF ─┘なんでわざわざ挟むの?
Webとスマホでは、欲しいものがけっこう違います。
| Web | スマホアプリ | |
|---|---|---|
| ログイン状態の持ち方 | Cookie | アプリ内にトークンを保存 |
| 1画面に必要なデータ | 画面が広いので多め | 画面が狭いので少なめ |
| 通信 | 比較的安定している | 回線が不安定なことも多く、通信回数を減らしたい |
1つのAPIでこれを全部まかなおうとすると、APIが「Webの都合」と「スマホの都合」でどんどん複雑になってしまいます。
そこで、クライアントごとの都合はBFFが引き受けて、共通のAPIはシンプルなままにする、というのがBFFの考え方です。
BFFがよくやること
- APIの呼び出しをまとめる:画面に必要なデータを複数のAPIから集めて、1回のレスポンスで返す
- データを画面に合わせて整形する:画面で使う形に変換して、余計なデータを送らない
- 認証を引き受ける:ログイン状態をCookieやセッションで管理して、APIにはトークンを付けて渡す
- 裏側を隠す:APIのURLや秘密の情報をブラウザに見せない
気をつけること
- 層が1つ増えるので、そのぶん通信が1回増えたり、運用するものが増えたりする
- BFFに業務ルールを書き始めると、Web用BFFとスマホ用BFFで同じロジックが重複してしまう
なので、業務ルールは共通のAPI側に置いて、BFFは「そのクライアントへの見せ方」だけを担当する、と役割を分けておくのが大事です。
このアプリでのBFF
このアプリでは、Next.jsのサーバー側(Server Component / Server Function)がそのままWeb用のBFFになっています。
BFF用のサーバーを別に立てなくていいのも、Next.jsを使うメリットです。
- ログイン状態はNext.jsのhttpOnly Cookieで持って、NestJSを呼ぶときに
Authorizationヘッダーに付け替える - NestJSのURLはNext.jsのサーバー側だけが知っていて、ブラウザには見せない
- 業務ルール(「自分自身はフォローできない」など)は、NestJSのService層に置く
という形で、上の「BFFがよくやること」と「気をつけること」をそのまま当てはめています。