はじめに
バックエンドの環境構築で時間を無駄にしていませんか?
「サーバーのセットアップに丸一日かかった」「認証システムの実装で躓いた」「リアルタイム機能の開発が複雑すぎる」そんな経験ありませんか?
この記事では、Googleが提供するFirebaseプラットフォームとTypeScriptを組み合わせて、わずか数時間で本格的なWebアプリケーションを構築する方法を解説します。サーバー管理不要、スケーリング自動、そして型安全な開発環境を手に入れましょう!
🎯 この記事で学べること
- Firebaseの主要サービスと特徴の理解
- Firebase SDK v9のTypeScript実装パターン
- Authentication、Firestore、Cloud Functionsの実践的な使い方
- リアルタイム機能の実装方法
- 本番環境デプロイのベストプラクティス
💡 Firebaseとは?開発者が知るべき5つの核心機能
Firebaseは、Googleが提供するBaaS(Backend as a Service)プラットフォームです。従来のバックエンド開発で必要だった複雑な設定を、すべてGoogleが肩代わりしてくれます。
主要サービス一覧
| サービス名 | 用途 | 特徴 |
|---|---|---|
| Authentication | ユーザー認証 | SNSログイン、メール認証、匿名認証対応 |
| Firestore | NoSQLデータベース | リアルタイム同期、オフライン対応 |
| Cloud Functions | サーバーレス関数 | イベント駆動、自動スケーリング |
| Hosting | 静的サイトホスティング | CDN、カスタムドメイン対応 |
| Storage | ファイルストレージ | 画像・動画の自動最適化 |
環境構築
1. プロジェクト初期化
{
"name": "firebase-typescript-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"serve": "firebase emulators:start"
},
"dependencies": {
"firebase": "^10.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"typescript": "^5.2.2",
"vite": "^5.0.8",
"firebase-tools": "^13.0.1"
}
}
2. Firebase CLI インストール&ログイン
npm install -g firebase-tools
firebase login
firebase init
3. Firebase設定ファイル
import { initializeApp } from 'firebase/app'
import { getAuth } from 'firebase/auth'
import { getFirestore } from 'firebase/firestore'
import { getFunctions } from 'firebase/functions'
const firebaseConfig = {
apiKey: process.env.VITE_FIREBASE_API_KEY,
authDomain: process.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: process.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: process.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.VITE_FIREBASE_APP_ID
}
const app = initializeApp(firebaseConfig)
export const auth = getAuth(app)
export const db = getFirestore(app)
export const functions = getFunctions(app)
export default app
4. 環境変数設定
VITE_FIREBASE_API_KEY=your-api-key
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your-project-id
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=123456789
VITE_FIREBASE_APP_ID=1:123456789:web:abcdef123456
Firebase Authentication
1. ユーザー認証のTypeScript実装
import { useState, useEffect } from 'react'
import {
User,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signInWithPopup,
GoogleAuthProvider,
signOut as firebaseSignOut,
onAuthStateChanged
} from 'firebase/auth'
import { auth } from '../lib/firebase'
interface AuthState {
user: User | null
loading: boolean
error: string | null
}
export const useAuth = () => {
const [state, setState] = useState<AuthState>({
user: null,
loading: true,
error: null
})
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setState({
user,
loading: false,
error: null
})
})
return unsubscribe
}, [])
const signInWithEmail = async (email: string, password: string) => {
try {
setState(prev => ({ ...prev, loading: true, error: null }))
await signInWithEmailAndPassword(auth, email, password)
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : '認証エラーが発生しました'
}))
}
}
const signUpWithEmail = async (email: string, password: string) => {
try {
setState(prev => ({ ...prev, loading: true, error: null }))
await createUserWithEmailAndPassword(auth, email, password)
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : '登録エラーが発生しました'
}))
}
}
const signInWithGoogle = async () => {
try {
setState(prev => ({ ...prev, loading: true, error: null }))
const provider = new GoogleAuthProvider()
await signInWithPopup(auth, provider)
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : 'Google認証エラーが発生しました'
}))
}
}
const signOut = async () => {
try {
await firebaseSignOut(auth)
} catch (error) {
setState(prev => ({
...prev,
error: error instanceof Error ? error.message : 'ログアウトエラーが発生しました'
}))
}
}
return {
...state,
signInWithEmail,
signUpWithEmail,
signInWithGoogle,
signOut
}
}
2. 認証コンポーネントの実装
import React, { useState } from 'react'
import { useAuth } from '../hooks/useAuth'
const Auth: React.FC = () => {
const [isSignUp, setIsSignUp] = useState(false)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const { user, loading, error, signInWithEmail, signUpWithEmail, signInWithGoogle, signOut } = useAuth()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (isSignUp) {
await signUpWithEmail(email, password)
} else {
await signInWithEmail(email, password)
}
}
if (loading) return <div>認証状態を確認中...</div>
if (user) {
return (
<div>
<h2>ようこそ、{user.displayName || user.email}さん!</h2>
<button onClick={signOut}>ログアウト</button>
</div>
)
}
return (
<div>
<h2>{isSignUp ? 'アカウント作成' : 'ログイン'}</h2>
{error && <div style={{ color: 'red' }}>{error}</div>}
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="メールアドレス"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="パスワード"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">
{isSignUp ? 'アカウント作成' : 'ログイン'}
</button>
</form>
<button onClick={signInWithGoogle}>
Googleでログイン
</button>
<button onClick={() => setIsSignUp(!isSignUp)}>
{isSignUp ? 'ログインに切り替え' : 'アカウント作成に切り替え'}
</button>
</div>
)
}
export default Auth
Firestore Database
型安全なリアルタイムデータベース
1. データモデルの型定義
export interface Post {
id: string
title: string
content: string
authorId: string
authorName: string
createdAt: Date
updatedAt: Date
tags: string[]
published: boolean
}
export interface Comment {
id: string
postId: string
authorId: string
authorName: string
content: string
createdAt: Date
}
export interface User {
id: string
displayName: string
email: string
photoURL?: string
createdAt: Date
}
2. Firestoreクライアントクラス
import {
collection,
doc,
getDocs,
getDoc,
addDoc,
updateDoc,
deleteDoc,
query,
where,
orderBy,
limit,
onSnapshot,
serverTimestamp,
Timestamp
} from 'firebase/firestore'
import { db } from './firebase'
import type { Post, Comment, User } from '../types'
class FirestoreService {
// Posts Collection
async createPost(post: Omit<Post, 'id' | 'createdAt' | 'updatedAt'>) {
try {
const docRef = await addDoc(collection(db, 'posts'), {
...post,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp()
})
return docRef.id
} catch (error) {
console.error('投稿作成エラー:', error)
throw error
}
}
async getPosts(userId?: string): Promise<Post[]> {
try {
const postsRef = collection(db, 'posts')
let q = query(
postsRef,
orderBy('createdAt', 'desc'),
limit(50)
)
if (userId) {
q = query(q, where('authorId', '==', userId))
}
const querySnapshot = await getDocs(q)
return querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
createdAt: (doc.data().createdAt as Timestamp).toDate(),
updatedAt: (doc.data().updatedAt as Timestamp).toDate()
} as Post))
} catch (error) {
console.error('投稿取得エラー:', error)
throw error
}
}
async getPost(postId: string): Promise<Post | null> {
try {
const docRef = doc(db, 'posts', postId)
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
const data = docSnap.data()
return {
id: docSnap.id,
...data,
createdAt: (data.createdAt as Timestamp).toDate(),
updatedAt: (data.updatedAt as Timestamp).toDate()
} as Post
}
return null
} catch (error) {
console.error('投稿取得エラー:', error)
throw error
}
}
async updatePost(postId: string, updates: Partial<Post>) {
try {
const docRef = doc(db, 'posts', postId)
await updateDoc(docRef, {
...updates,
updatedAt: serverTimestamp()
})
} catch (error) {
console.error('投稿更新エラー:', error)
throw error
}
}
async deletePost(postId: string) {
try {
const docRef = doc(db, 'posts', postId)
await deleteDoc(docRef)
} catch (error) {
console.error('投稿削除エラー:', error)
throw error
}
}
// リアルタイム購読
subscribeToPostsRealtime(callback: (posts: Post[]) => void, userId?: string) {
const postsRef = collection(db, 'posts')
let q = query(
postsRef,
orderBy('createdAt', 'desc'),
limit(50)
)
if (userId) {
q = query(q, where('authorId', '==', userId))
}
return onSnapshot(q, (querySnapshot) => {
const posts: Post[] = querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
createdAt: (doc.data().createdAt as Timestamp).toDate(),
updatedAt: (doc.data().updatedAt as Timestamp).toDate()
} as Post))
callback(posts)
})
}
}
export const firestoreService = new FirestoreService()
3. Firestoreを使用するReactコンポーネント
import React, { useState, useEffect } from 'react'
import { firestoreService } from '../lib/firestore'
import { useAuth } from '../hooks/useAuth'
import type { Post } from '../types'
const PostList: React.FC = () => {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
const [newPost, setNewPost] = useState({ title: '', content: '', tags: '' })
const { user } = useAuth()
useEffect(() => {
if (!user) return
// リアルタイム購読
const unsubscribe = firestoreService.subscribeToPostsRealtime((posts) => {
setPosts(posts)
setLoading(false)
})
return unsubscribe
}, [user])
const handleCreatePost = async (e: React.FormEvent) => {
e.preventDefault()
if (!user || !newPost.title.trim()) return
try {
await firestoreService.createPost({
title: newPost.title,
content: newPost.content,
authorId: user.uid,
authorName: user.displayName || user.email || '匿名ユーザー',
tags: newPost.tags.split(',').map(tag => tag.trim()).filter(Boolean),
published: true
})
setNewPost({ title: '', content: '', tags: '' })
} catch (error) {
console.error('投稿作成に失敗しました:', error)
}
}
if (loading) return <div>読み込み中...</div>
return (
<div>
<h2>投稿一覧</h2>
{user && (
<form onSubmit={handleCreatePost}>
<h3>新しい投稿</h3>
<input
type="text"
placeholder="タイトル"
value={newPost.title}
onChange={(e) => setNewPost(prev => ({ ...prev, title: e.target.value }))}
required
/>
<textarea
placeholder="内容"
value={newPost.content}
onChange={(e) => setNewPost(prev => ({ ...prev, content: e.target.value }))}
rows={4}
/>
<input
type="text"
placeholder="タグ (カンマ区切り)"
value={newPost.tags}
onChange={(e) => setNewPost(prev => ({ ...prev, tags: e.target.value }))}
/>
<button type="submit">投稿する</button>
</form>
)}
<div>
{posts.map(post => (
<div key={post.id} style={{ border: '1px solid #ccc', margin: '10px', padding: '10px' }}>
<h3>{post.title}</h3>
<p>{post.content}</p>
<small>
作成者: {post.authorName} |
作成日: {post.createdAt.toLocaleDateString()}
</small>
{post.tags.length > 0 && (
<div>
タグ: {post.tags.map(tag => (
<span key={tag} style={{ backgroundColor: '#f0f0f0', padding: '2px 6px', margin: '2px', borderRadius: '3px' }}>
{tag}
</span>
))}
</div>
)}
</div>
))}
</div>
</div>
)
}
export default PostList
Cloud Functions:サーバーレス関数の実装
1. Functions プロジェクトセットアップ
import { onRequest, onCall } from 'firebase-functions/v2/https'
import { onDocumentCreated, onDocumentUpdated } from 'firebase-functions/v2/firestore'
import { initializeApp } from 'firebase-admin/app'
import { getFirestore } from 'firebase-admin/firestore'
import { getAuth } from 'firebase-admin/auth'
initializeApp()
const db = getFirestore()
const auth = getAuth()
// HTTP関数:投稿の統計情報を取得
export const getPostStats = onRequest({ cors: true }, async (req, res) => {
try {
const postsSnapshot = await db.collection('posts').get()
const publishedCount = postsSnapshot.docs.filter(doc => doc.data().published).length
const totalCount = postsSnapshot.size
const authorsSnapshot = await db.collection('posts')
.where('published', '==', true)
.get()
const uniqueAuthors = new Set(authorsSnapshot.docs.map(doc => doc.data().authorId))
res.json({
totalPosts: totalCount,
publishedPosts: publishedCount,
uniqueAuthors: uniqueAuthors.size
})
} catch (error) {
console.error('統計取得エラー:', error)
res.status(500).json({ error: '統計情報の取得に失敗しました' })
}
})
// Callable関数:ユーザープロファイル更新
export const updateUserProfile = onCall(async (request) => {
const { auth: userAuth, data } = request
if (!userAuth) {
throw new Error('認証が必要です')
}
try {
const { displayName, photoURL } = data
// Firebase Auth のプロファイル更新
await auth.updateUser(userAuth.uid, {
displayName,
photoURL
})
// Firestore のユーザードキュメント更新
await db.collection('users').doc(userAuth.uid).set({
displayName,
photoURL,
email: userAuth.token.email,
updatedAt: new Date()
}, { merge: true })
return { success: true, message: 'プロファイルが更新されました' }
} catch (error) {
console.error('プロファイル更新エラー:', error)
throw new Error('プロファイルの更新に失敗しました')
}
})
// Firestore トリガー:投稿作成時の通知
export const onPostCreated = onDocumentCreated('posts/{postId}', async (event) => {
const snapshot = event.data
if (!snapshot) return
const post = snapshot.data()
const postId = snapshot.id
try {
// 投稿作成の通知ログを作成
await db.collection('notifications').add({
type: 'new_post',
postId,
authorId: post.authorId,
authorName: post.authorName,
postTitle: post.title,
createdAt: new Date()
})
console.log(`新しい投稿の通知を作成しました: ${postId}`)
} catch (error) {
console.error('通知作成エラー:', error)
}
})
// Firestore トリガー:投稿更新時の処理
export const onPostUpdated = onDocumentUpdated('posts/{postId}', async (event) => {
const beforeData = event.data?.before.data()
const afterData = event.data?.after.data()
if (!beforeData || !afterData) return
// 公開状態が変更された場合
if (beforeData.published !== afterData.published) {
const postId = event.params.postId
try {
await db.collection('post_history').add({
postId,
action: afterData.published ? 'published' : 'unpublished',
authorId: afterData.authorId,
timestamp: new Date()
})
console.log(`投稿の公開状態が変更されました: ${postId} -> ${afterData.published}`)
} catch (error) {
console.error('履歴記録エラー:', error)
}
}
})
2. フロントエンドからCloud Functionsを呼び出し
import { httpsCallable } from 'firebase/functions'
import { functions } from '../lib/firebase'
interface UpdateProfileData {
displayName: string
photoURL?: string
}
interface UpdateProfileResult {
success: boolean
message: string
}
export const useFunctions = () => {
const updateUserProfile = httpsCallable<UpdateProfileData, UpdateProfileResult>(
functions,
'updateUserProfile'
)
const callUpdateProfile = async (data: UpdateProfileData) => {
try {
const result = await updateUserProfile(data)
return result.data
} catch (error) {
console.error('プロファイル更新エラー:', error)
throw error
}
}
const getPostStats = async () => {
try {
const response = await fetch(`https://your-region-your-project.cloudfunctions.net/getPostStats`)
return await response.json()
} catch (error) {
console.error('統計取得エラー:', error)
throw error
}
}
return {
callUpdateProfile,
getPostStats
}
}
デプロイとセキュリティルール
1. Firestoreセキュリティルール
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users collection
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
allow read: if request.auth != null;
}
// Posts collection
match /posts/{postId} {
allow read: if resource.data.published == true ||
(request.auth != null && request.auth.uid == resource.data.authorId);
allow create: if request.auth != null &&
request.auth.uid == request.resource.data.authorId;
allow update: if request.auth != null &&
request.auth.uid == resource.data.authorId;
allow delete: if request.auth != null &&
request.auth.uid == resource.data.authorId;
}
// Comments collection
match /comments/{commentId} {
allow read: if request.auth != null;
allow create: if request.auth != null &&
request.auth.uid == request.resource.data.authorId;
allow update, delete: if request.auth != null &&
request.auth.uid == resource.data.authorId;
}
// Notifications collection (read-only for users)
match /notifications/{notificationId} {
allow read: if request.auth != null;
}
}
}
2. Firebase Hosting設定
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
],
"headers": [
{
"source": "**/*.@(js|css)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=31536000"
}
]
}
]
},
"functions": {
"source": "functions",
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log"
]
},
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
}
}
3. デプロイ
# ビルド
npm run build
# Functions デプロイ
firebase deploy --only functions
# Hosting デプロイ
firebase deploy --only hosting
# すべてデプロイ
firebase deploy
まとめ
Firebase × TypeScriptの組み合わせで、以下を実現できました:
✅ 認証システム - メール認証、SNSログイン、状態管理
✅ リアルタイムデータベース - 型安全なFirestore操作
✅ サーバーレス関数 - イベント駆動の処理
✅ セキュリティルール - データアクセス制御
✅ 本番デプロイ - CDN、カスタムドメイン対応
この実装パターンを使えば、サーバー管理不要で スケーラブルなアプリケーションをすぐに構築開始できますね!
次のステップとして、Firebase Analytics や Cloud Messaging を組み込んで、さらに高機能なアプリケーションを作ってみてください!