はじめに
初めてのAI Codingに挑戦してみました。結果、なんとわずか10分でSNSアプリが完成!
私がしたことは、VSCodeを開き、拡張機能のIBM Bobをインストールして「SNSアプリを作りたい」と入力しただけ。
あとは、Bobがコードを書きながら「ここはこういうことをしているんだよ」と解説してくれるのを見守るだけでした。
今回は、Bobがどのくらい本格的なWebアプリを自動生成できるのかを、スクショとコード例とともに紹介します。
初心者でもここまでできる!という驚きと、AI活用のリアルな体験を共有できればと思います。
実装機能の紹介
作成したSNS風Webアプリケーションには、驚くほど充実した機能が備わっています:
- ✅ ユーザー認証(登録・ログイン)
- ✅ 投稿の作成・編集・削除
- ✅ 画像付き投稿
- ✅ いいね機能
- ✅ コメント機能
- ✅ フォロー/フォロワー機能
- ✅ タイムライン表示
- ✅ プロフィール編集
- ✅ ユーザー検索
デモ
機能が充実しているのはもちろんですが、それ以上にUIの美しさが際立っています。
初心者が作ったとは思えない、見た目も使い勝手も整ったアプリに仕上がりました。
②新規登録画面
ユーザー名やメールアドレスなどの個人情報を登録します。

③ログイン画面
新規登録ができたら、ここからログインすることができます。

④投稿画面
Xのように、文章や写真などを投稿することができます。

⑤プロフィール画面
編集ボタンを押すと、背景画像やプロフィール画像、名前、自己紹介などを編集することができます。

⑥タイムライン
フォローした人や自分の投稿が時系列順に表示され、いいねやコメントができます。

技術スタック
- バックエンド: Node.js v14+, Express v4
- データベース: MongoDB v4+
- テンプレートエンジン: EJS
- 認証: Passport.js (Local Strategy)
- 画像アップロード: Multer, Cloudinary
- セッション管理: express-session, connect-mongo
- その他: bcrypt, method-override, express-validator
プロジェクト構成
sns-app/
├── config/ # 設定ファイル
│ ├── database.js # MongoDB接続
│ └── passport.js # Passport認証設定
├── controllers/ # ビジネスロジック
│ ├── authController.js
│ ├── postController.js
│ └── userController.js
├── middleware/ # カスタムミドルウェア
│ └── auth.js
├── models/ # データモデル
│ ├── User.js
│ └── Post.js
├── public/ # 静的ファイル
│ ├── css/
│ ├── js/
│ └── images/
├── routes/ # ルート定義
│ ├── auth.js
│ ├── posts.js
│ └── users.js
├── views/ # EJSテンプレート
│ ├── auth/
│ ├── posts/
│ ├── users/
│ └── partials/
├── .env # 環境変数
├── package.json
└── server.js # エントリーポイント
セットアップ
1. プロジェクトの初期化
mkdir sns-app
cd sns-app
npm init -y
2. 必要なパッケージのインストール
npm install express mongoose ejs express-session connect-mongo passport passport-local bcrypt dotenv express-flash express-validator method-override multer cloudinary
npm install --save-dev nodemon
3. package.jsonのスクリプト設定
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}
主要な実装
1. サーバーのセットアップ (server.js)
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const passport = require('passport');
const flash = require('express-flash');
const methodOverride = require('method-override');
const app = express();
// データベース接続
require('./config/database');
// Passport設定
require('./config/passport')(passport);
// ミドルウェア設定
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(methodOverride('_method'));
app.use(express.static('public'));
// ビューエンジン設定
app.set('view engine', 'ejs');
// セッション設定
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI
})
})
);
// Passport初期化
app.use(passport.initialize());
app.use(passport.session());
app.use(flash());
// ルート設定
app.use('/', require('./routes/auth'));
app.use('/posts', require('./routes/posts'));
app.use('/users', require('./routes/users'));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
2. ユーザーモデル (models/User.js)
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true
},
displayName: String,
bio: String,
profileImage: {
type: String,
default: '/images/default-avatar.svg'
},
followers: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
following: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
}, {
timestamps: true
});
// パスワードをハッシュ化
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
module.exports = mongoose.model('User', userSchema);
3. 投稿モデル (models/Post.js)
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
content: {
type: String,
required: true,
maxlength: 500
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
image: String,
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
comments: [{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
content: String,
createdAt: {
type: Date,
default: Date.now
}
}]
}, {
timestamps: true
});
module.exports = mongoose.model('Post', postSchema);
4. Passport認証設定 (config/passport.js)
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/User');
module.exports = function(passport) {
passport.use(
new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
try {
const user = await User.findOne({ email: email.toLowerCase() });
if (!user) {
return done(null, false, { message: 'メールアドレスまたはパスワードが正しくありません' });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return done(null, false, { message: 'メールアドレスまたはパスワードが正しくありません' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}
)
);
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (error) {
done(error);
}
});
};
5. 認証ミドルウェア (middleware/auth.js)
exports.ensureAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return next();
}
req.flash('error', 'ログインが必要です');
res.redirect('/login');
};
exports.ensureGuest = (req, res, next) => {
if (!req.isAuthenticated()) {
return next();
}
res.redirect('/posts');
};
主要機能の実装
いいね機能
// controllers/postController.js
exports.likePost = async (req, res) => {
try {
const post = await Post.findById(req.params.id);
const likeIndex = post.likes.indexOf(req.user._id);
if (likeIndex === -1) {
post.likes.push(req.user._id);
} else {
post.likes.splice(likeIndex, 1);
}
await post.save();
res.json({
success: true,
likeCount: post.likes.length,
isLiked: likeIndex === -1
});
} catch (error) {
res.status(500).json({ error: 'エラーが発生しました' });
}
};
フォロー機能
// controllers/userController.js
exports.followUser = async (req, res) => {
try {
const userToFollow = await User.findById(req.params.id);
const currentUser = await User.findById(req.user._id);
const isFollowing = currentUser.following.includes(userToFollow._id);
if (isFollowing) {
currentUser.following.pull(userToFollow._id);
userToFollow.followers.pull(currentUser._id);
} else {
currentUser.following.push(userToFollow._id);
userToFollow.followers.push(currentUser._id);
}
await currentUser.save();
await userToFollow.save();
res.json({
success: true,
isFollowing: !isFollowing,
followerCount: userToFollow.followers.length
});
} catch (error) {
res.status(500).json({ error: 'エラーが発生しました' });
}
};
フロントエンド
EJSテンプレート例
<!-- views/posts/timeline.ejs -->
<%- include('../partials/header', { title: 'タイムライン' }) %>
<div class="timeline-container">
<% posts.forEach(function(post) { %>
<div class="post-card">
<div class="post-header">
<a href="/users/<%= post.author._id %>">
<img src="<%= post.author.profileImage %>" alt="<%= post.author.username %>">
<span><%= post.author.displayName %></span>
</a>
</div>
<div class="post-content">
<p><%= post.content %></p>
<% if (post.image) { %>
<img src="<%= post.image %>" alt="投稿画像">
<% } %>
</div>
<div class="post-actions">
<button class="like-btn" data-post-id="<%= post._id %>">
<i class="fas fa-heart"></i>
<span><%= post.likes.length %></span>
</button>
</div>
</div>
<% }); %>
</div>
<%- include('../partials/footer') %>
JavaScript (いいね機能)
// public/js/main.js
document.querySelectorAll('.like-btn').forEach(button => {
button.addEventListener('click', async function() {
const postId = this.dataset.postId;
try {
const response = await fetch(`/posts/${postId}/like`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (data.success) {
const icon = this.querySelector('i');
const count = this.querySelector('span');
icon.classList.toggle('fas');
icon.classList.toggle('far');
count.textContent = data.likeCount;
}
} catch (error) {
console.error('Error:', error);
}
});
});
環境変数の設定
MONGODB_URI=mongodb://localhost:27017/sns-app
SESSION_SECRET=your-secret-key-here
PORT=3000
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secret
起動方法
# MongoDBを起動
# Windows: net start MongoDB
# macOS/Linux: sudo systemctl start mongod
# アプリケーションを起動
npm run dev
# ブラウザでアクセス
# http://localhost:3000
セキュリティ対策
- パスワードのハッシュ化: bcryptを使用
- セッション管理: MongoDBにセッションを保存
- CSRF対策: method-overrideを使用
- バリデーション: express-validatorで入力検証
- 環境変数: 機密情報は.envファイルで管理
今後の拡張案
- リアルタイム通知機能(Socket.io)
- ダイレクトメッセージ機能
- ハッシュタグ機能
- トレンド表示
- メール認証
- パスワードリセット機能
- 二段階認証
- ダークモード
まとめ
この記事では、Node.js + Express + MongoDB を使ってSNS風Webアプリを作成しました。
初心者でも10分でここまでできるとは、正直自分でも驚きです 😳
主なポイントは以下の通りです:
- MVCアーキテクチャで構造化されたコード
- Passport.js による安全な認証機能
- MongoDB でのユーザー・投稿リレーション管理
- EJSテンプレートエンジンで簡単にビューを構築
- RESTful API設計で拡張もしやすい
完全なソースコードはGitHubリポジトリで公開中ですので、ぜひ触ってみてください!
参考リンク
おわりに
ちなみに、この記事もほぼBobに書いてもらいました笑😆
- 作成時間はたったの10分
- エラーもなし
- 私はVSCodeでBobの指示を眺めていただけ
初心者でも「AIと一緒に作る」だけで、ここまで本格的なアプリが作れるのは衝撃的です。
次はどんなアプリを作ろうかなあ。
みなさんもぜひBobを使って、自分だけのアプリを作ってみてください!
質問・感想・作ってみた報告などのコメント、たくさんお待ちしております!💡
Happy Coding! 🚀
