Full-Stack Fusion: React, Fastify, & Firebase with TypeScript
Building modern web applications demands a stack that offers speed, scalability, and a superior developer experience. This post demonstrates how to integrate React for dynamic UIs, Fastify for a high-performance backend, and Firebase for backend services, all powered by TypeScript.
The Integrated Architecture
This architecture leverages React for the frontend, providing a component-based UI. Fastify serves as the API layer, handling business logic and secure interactions. Firebase provides authentication, real-time database (Firestore), and other essential services, minimizing infrastructure overhead.
Fastify Backend: High-Performance API
Fastify is a fast and low-overhead web framework for Node.js, ideal for building robust APIs. We'll use it to expose secure endpoints and interact with Firebase Admin SDK.
First, set up a basic Fastify server with TypeScript and install @fastify/cors to handle cross-origin requests from your React app. Install firebase-admin to interact with Firebase services securely from the backend.
// src/server.ts
import Fastify from 'fastify';
import cors from '@fastify/cors';
import admin from 'firebase-admin';
import * as dotenv from 'dotenv';
dotenv.config();
// Initialize Firebase Admin SDK (replace with your service account key path or config)
// Ensure FIREBASE_SERVICE_ACCOUNT_KEY is a stringified JSON object in your .env
const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_KEY || '{}');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
const fastify = Fastify({ logger: true });
fastify.register(cors, {
origin: 'http://localhost:3000' // Your React app's origin
});
interface AuthenticatedRequest extends Fastify.FastifyRequest {
user?: admin.auth.DecodedIdToken;
}
// Pre-handler hook for authentication
fastify.addHook('preHandler', async (request: AuthenticatedRequest, reply) => {
try {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new Error('No authorization token provided.');
}
const idToken = authHeader.split('Bearer ')[1];
const decodedToken = await admin.auth().verifyIdToken(idToken);
request.user = decodedToken;
} catch (error: any) {
reply.code(401).send({ message: error.message || 'Unauthorized' });
throw new Error('Unauthorized'); // Stop further processing
}
});
fastify.get('/api/secure-data', async (request: AuthenticatedRequest, reply) => {
// Access authenticated user via request.user
const userId = request.user?.uid;
const userEmail = request.user?.email;
// Example: Fetch data from Firestore
const docRef = admin.firestore().collection('users').doc(userId);
const doc = await docRef.get();
if (doc.exists) {
return { message: `Hello, ${userEmail}! Here's your data:`, data: doc.data() };
} else {
// Create a new user document if it doesn't exist
await docRef.set({ email: userEmail, createdAt: admin.firestore.FieldValue.serverTimestamp() });
return { message: `Welcome, ${userEmail}! New user data created.`, data: { email: userEmail } };
}
});
const start = async () => {
try {
await fastify.listen({ port: 3001 });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
This Fastify server includes a pre-handler hook to verify Firebase ID tokens, securing your API endpoints. It then uses the Firebase Admin SDK to interact with Firestore, demonstrating how to fetch or create user-specific data.
React Frontend: Dynamic User Interface
React builds interactive user interfaces efficiently. We'll use the Firebase Client SDK for user authentication and fetch to communicate with our Fastify backend.
Install firebase and react-firebase-hooks (optional, but useful for auth state).
// src/firebaseConfig.ts
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
// src/App.tsx
import React, { useState, useEffect } from 'react';
import { signInWithPopup, GoogleAuthProvider, signOut, User } from 'firebase/auth';
import { auth } from './firebaseConfig';
function App() {
const [user, setUser] = useState<User | null>(null);
const [data, setData] = useState<any | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((currentUser) => {
setUser(currentUser);
setData(null); // Clear data on auth state change
});
return () => unsubscribe();
}, []);
const handleGoogleSignIn = async () => {
const provider = new GoogleAuthProvider();
try {
await signInWithPopup(auth, provider);
} catch (err: any) {
console.error(err);
setError(err.message);
}
};
const handleSignOut = async () => {
try {
await signOut(auth);
} catch (err: any) {
console.error(err);
setError(err.message);
}
};
const fetchSecureData = async () => {
if (!user) return;
setLoading(true);
setError(null);
try {
const idToken = await user.getIdToken();
const response = await fetch('http://localhost:3001/api/secure-data', {
headers: {
Authorization: `Bearer ${idToken}`,
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err: any) {
console.error('Error fetching secure data:', err);
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>React + Fastify + Firebase App</h1>
{!user ? (
<button onClick={handleGoogleSignIn}>Sign in with Google</button>
) : (
<div>
<p>Welcome, {user.displayName || user.email}!</p>
<button onClick={handleSignOut}>Sign Out</button>
<button onClick={fetchSecureData} disabled={loading} style={{ marginLeft: '10px' }}>
{loading ? 'Loading...' : 'Fetch Secure Data'}
</button>
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
{data && (
<div style={{ marginTop: '20px', border: '1px solid #ccc', padding: '10px' }}>
<h3>Secure Data from Fastify:</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
</div>
)}
</div>
);
}
export default App;
This React component handles user authentication via Firebase's client SDK. Once authenticated, it retrieves the user's ID token and sends it to the Fastify backend to access a protected endpoint. The backend then verifies this token and returns user-specific data.
Firebase: Authentication & Data Persistence
Firebase provides scalable backend services without managing servers. Firebase Authentication handles user sign-up/sign-in, while Firestore offers a flexible, NoSQL cloud database.
- Authentication: The React frontend uses
firebase/authfor client-side authentication flows (e.g., Google Sign-In). The Fastify backend usesfirebase-adminto verify the authenticity of user tokens, ensuring only legitimate requests access protected resources. - Firestore: Both client and server can interact with Firestore. The Fastify backend, with its elevated privileges via
firebase-admin, can perform server-side data operations (e.g., creating/updating user profiles, sensitive data access). The React frontend can also directly access Firestore for less sensitive, user-specific data, often secured by Firestore Security Rules.
Conclusion
By integrating React, Fastify, and Firebase with TypeScript, you create a powerful, type-safe, and scalable full-stack application. React delivers a rich user experience, Fastify ensures a performant and secure API layer, and Firebase handles the heavy lifting of authentication and data management, allowing developers to focus on core application logic. This stack provides a robust foundation for modern web development, ready for complex features and deployment to platforms like Vercel (for React) and Cloud Run (for Fastify).