cd..blog

Building Modern Web Apps: Next.js and Firebase Powerhouse

const published = "Oct 30, 2025, 10:22 PM";const readTime = 5 min;
nextjsfirebasetypescriptfullstackwebdev
Learn to seamlessly integrate Next.js with Firebase for powerful, scalable, full-stack applications using TypeScript and best practices.

Building Modern Web Apps: Next.js and Firebase Powerhouse

Modern web development demands speed, scalability, and an exceptional developer experience. Combining Next.js for its powerful React framework capabilities and Firebase for its robust backend services creates an incredibly efficient and scalable full-stack solution. This guide demonstrates how to integrate these two technologies using TypeScript, providing practical examples for intermediate to advanced developers.

1. Project Setup: Next.js & Firebase Initialization

First, set up your Next.js project with TypeScript and install the Firebase client SDK. Next.js provides server-side rendering (SSR), static site generation (SSG), and API routes, while Firebase offers authentication, databases, storage, and more.

# Create a new Next.js project with the App Router and TypeScript
npx create-next-app@latest my-firebase-app --typescript --app
cd my-firebase-app

# Install Firebase client SDK
npm install firebase

Next, initialize Firebase. Create a firebase.ts utility file to centralize your Firebase configuration. Retrieve your project's configuration from the Firebase Console.

// src/lib/firebase.ts
import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

// Initialize Firebase for client-side use
const app = !getApps().length ? initializeApp(firebaseConfig) : getApp();

export const auth = getAuth(app);
export const db = getFirestore(app);

Remember to define these NEXT_PUBLIC_ environment variables in a .env.local file.

2. Firebase Authentication with Next.js

Firebase Authentication simplifies user management, offering various sign-in methods. We'll implement a basic Google Sign-In using the client-side SDK and manage user state with React Context.

// src/context/AuthContext.tsx
"use client";

import { createContext, useContext, useEffect, useState, ReactNode }
  from "react";
import { User, onAuthStateChanged, GoogleAuthProvider, signInWithPopup }
  from "firebase/auth";
import { auth } from "@/lib/firebase";

interface AuthContextType {
  user: User | null;
  loading: boolean;
  signInWithGoogle: () => Promise<void>;
  signOut: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });
    return () => unsubscribe();
  }, []);

  const signInWithGoogle = async () => {
    const provider = new GoogleAuthProvider();
    await signInWithPopup(auth, provider);
  };

  const signOut = async () => {
    await auth.signOut();
  };

  return (
    <AuthContext.Provider value={{ user, loading, signInWithGoogle, signOut }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
};

Wrap your layout.tsx or specific components with <AuthProvider>. You can then use useAuth() to access the user's authentication state and methods.

3. Firestore Data Management

Cloud Firestore is a flexible, scalable NoSQL cloud database. Here's how to add and fetch data, demonstrating real-time capabilities.

// src/components/TodoManager.tsx
"use client";

import { useEffect, useState } from "react";
import { collection, addDoc, query, onSnapshot, DocumentData, orderBy }
  from "firebase/firestore";
import { db } from "@/lib/firebase";
import { useAuth } from "@/context/AuthContext";

interface TodoItem extends DocumentData {
  id: string;
  text: string;
  userId: string;
  createdAt: Date;
}

export default function TodoManager() {
  const { user } = useAuth();
  const [todos, setTodos] = useState<TodoItem[]>([]);
  const [newTodoText, setNewTodoText] = useState("");

  useEffect(() => {
    if (!user) return;

    const q = query(
      collection(db, "todos"),
      orderBy("createdAt", "desc")
    );

    const unsubscribe = onSnapshot(q, (snapshot) => {
      const todosData: TodoItem[] = [];
      snapshot.forEach((doc) => {
        const data = doc.data() as Omit<TodoItem, 'id'>;
        if (data.userId === user.uid) { // Filter by current user
          todosData.push({ id: doc.id, ...data, createdAt: data.createdAt.toDate() });
        }
      });
      setTodos(todosData);
    });

    return () => unsubscribe();
  }, [user]);

  const addTodo = async () => {
    if (newTodoText.trim() === "" || !user) return;
    await addDoc(collection(db, "todos"), {
      text: newTodoText,
      userId: user.uid,
      createdAt: new Date(),
    });
    setNewTodoText("");
  };

  return (
    <div>
      <input
        type="text"
        value={newTodoText}
        onChange={(e) => setNewTodoText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button onClick={addTodo}>Add Todo</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
}

This component fetches and displays user-specific todos in real-time. Remember to set up appropriate Firestore Security Rules to protect your data, e.g., allow read, write: if request.auth.uid == resource.data.userId;.

4. Server-Side Interactions with Firebase Admin SDK

For privileged operations or sensitive data, use the Firebase Admin SDK within Next.js API Routes or Server Components. This ensures your Firebase service account credentials remain secure on the server.

// src/app/api/verify-token/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth as adminAuth } from '@/lib/firebase-admin'; // Admin SDK setup

export async function POST(req: NextRequest) {
  try {
    const { idToken } = await req.json();

    if (!idToken) {
      return NextResponse.json({ error: 'ID token is required' }, { status: 400 });
    }

    const decodedToken = await adminAuth.verifyIdToken(idToken);
    // You can now access decodedToken.uid or other claims
    return NextResponse.json({ uid: decodedToken.uid, email: decodedToken.email });
  } catch (error: any) {
    console.error('Error verifying ID token:', error);
    return NextResponse.json({ error: error.message }, { status: 401 });
  }
}

To use the Admin SDK, create a src/lib/firebase-admin.ts file, initializing it with your service account credentials. This file should only be imported on the server-side (e.g., API Routes, Server Components).

// src/lib/firebase-admin.ts (DO NOT IMPORT THIS ON THE CLIENT-SIDE)
import { initializeApp, getApps, getApp, cert } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
import { getFirestore } from 'firebase-admin/firestore';

// Load your service account key from an environment variable
// Ensure this is a JSON string, e.g., process.env.FIREBASE_SERVICE_ACCOUNT_KEY = JSON.stringify({ ... });
const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_KEY || '{}');

const adminApp = !getApps().length
  ? initializeApp({
      credential: cert(serviceAccount),
      projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, // Use client-side project ID
    })
  : getApp();

export const auth = getAuth(adminApp);
export const db = getFirestore(adminApp);

Conclusion

Integrating Next.js and Firebase with TypeScript provides a robust, type-safe, and highly scalable foundation for modern web applications. From seamless authentication to real-time data management and secure server-side operations, this powerful combination empowers developers to build feature-rich experiences efficiently. Embrace these tools to accelerate your development workflow and deliver exceptional applications.