LogoStarterkitpro
Walkthrough

API Routes

Creating Public, Protected, and Paid API Endpoints

Next.js API Routes allow you to create backend endpoints directly within your Next.js application. Any file named route.ts (or route.js) inside the /app/api directory structure becomes an API endpoint.

StarterKitPro utilizes NextAuth.js(Authjs) for handling authentication seamlessly via secure cookies.

API Client Helper

StarterKitPro includes an apiClient helper (lib/api.ts) built on Axios. It's recommended for frontend API calls as it automatically: - Sets the base URL to /api. - Redirects to the login page on 401 Unauthorized errors. - Redirects to the billing page on 403 Forbidden errors (for paid routes). - Displays other API errors using toast notifications.

Creating API Routes

Here's how to structure different types of API routes:

1. Public API Route

These routes are accessible to anyone, without requiring authentication.

Example Backend:

app/api/public-data/route.ts
import { NextResponse } from "next/server";
 
// Example: Fetching public data
export async function GET() {
  try {
    // Replace with your actual data fetching logic
    const publicData = { message: "This data is public!" };
    return NextResponse.json(publicData);
  } catch (error) {
    console.error("Error fetching public data:", error);
    return NextResponse.json(
      { error: "Failed to fetch data" },
      { status: 500 }
    );
  }
}

Example Frontend Call:

components/public-data-fetcher.tsx
"use client";
 
import { useState } from "react";
import apiClient from "@/lib/api";
import { Button } from "@/components/ui/button";
 
function PublicDataFetcher() {
  const [data, setData] = useState<any>(null);
  const [isLoading, setIsLoading] = useState(false);
 
  const fetchPublicData = async () => {
    setIsLoading(true);
    setData(null);
    try {
      const response = await apiClient.get("/public-data");
      setData(response);
      console.log("Public Data:", response);
    } catch (error) {
      // apiClient handles default errors, but custom logic can go here
      console.error("Custom Error Handling for Public Data Fetch:", error);
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    <div>
      <Button onClick={fetchPublicData} disabled={isLoading}>
        {isLoading ? "Loading Public Data..." : "Fetch Public Data"}
      </Button>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}
 
export default PublicDataFetcher;

2. Protected API Route (Authentication Required)

These routes require the user to be signed in.

Automatic Session Handling

NextAuth.js handles session management via secure HTTP-only cookies. This means you don't need to manually pass authentication tokens from the frontend when using the apiClient or standard browser fetch requests, as the browser automatically includes the cookie.

Example Backend:

app/api/user/profile/route.ts
import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/session"; // Helper to get session
import { prisma } from "@/lib/db"; // Prisma client instance
 
export async function GET() {
  const user = await getCurrentUser();
 
  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  try {
    // Fetch user-specific profile data from DB
    const userProfile = await prisma.user.findUnique({
      where: { id: user.id },
      select: { email: true, name: true /* other fields */ },
    });
 
    if (!userProfile) {
      return NextResponse.json(
        { error: "User profile not found" },
        { status: 404 }
      );
    }
 
    return NextResponse.json(userProfile);
  } catch (error) {
    console.error("Error fetching profile:", error);
    return NextResponse.json(
      { error: "Failed to fetch profile" },
      { status: 500 }
    );
  }
}

Example Frontend Call:

components/user-profile-fetcher.tsx
"use client";
 
import { useState } from "react";
import apiClient from "@/lib/api";
import { Button } from "@/components/ui/button";
 
function UserProfileFetcher() {
  const [profile, setProfile] = useState<any>(null);
  const [isLoading, setIsLoading] = useState(false);
 
  const fetchUserProfile = async () => {
    setIsLoading(true);
    setProfile(null);
    try {
      // apiClient handles authentication automatically via cookies
      const response = await apiClient.get("/user/profile");
      setProfile(response);
      console.log("Profile:", response);
    } catch (error) {
      // apiClient handles 401 redirects, but custom logic can go here
      console.error("Custom Error Handling for Profile Fetch:", error);
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    <div>
      <Button onClick={fetchUserProfile} disabled={isLoading}>
        {isLoading ? "Loading Profile..." : "Fetch User Profile"}
      </Button>
      {profile && <pre>{JSON.stringify(profile, null, 2)}</pre>}
    </div>
  );
}
 
export default UserProfileFetcher;

3. Paid API Route (Authentication + Access Required)

These routes require the user to be signed in AND have active access (e.g., a subscription or one-time purchase).

Checking Access Efficiently

The backend example checks user.hasAccess from the curret user. Because we hasAccess to true in webhook.

Example Backend:

app/api/premium-content/route.ts
import { NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/session";
import { prisma } from "@/lib/db";
 
export async function GET() {
  const user = await getCurrentUser();
 
  // 1. Check Authentication
  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  // 2. Check Access (using a field like 'hasAccess' set by webhooks)  if (!user?.hasAccess) {
    return NextResponse.json(
      { error: "Forbidden: Access required" },
      { status: 403 }
    );
  }
 
  // 3. User is authenticated and has access, proceed
  try {
    const premiumData = { message: "Welcome to the premium zone!" };
    return NextResponse.json(premiumData);
  } catch (error) {
    console.error("Error fetching premium content:", error);
    return NextResponse.json(
      { error: "Failed to fetch premium content" },
      { status: 500 }
    );
  }
}

Example Frontend Call:

components/premium-content-fetcher.tsx
"use client";
 
import { useState } from "react";
import apiClient from "@/lib/api";
import { Button } from "@/components/ui/button";
 
function PremiumContentFetcher() {
  const [content, setContent] = useState<any>(null);
  const [isLoading, setIsLoading] = useState(false);
 
  const fetchPremiumContent = async () => {
    setIsLoading(true);
    setContent(null);
    try {
      const response = await apiClient.get("/premium-content");
      setContent(response);
      console.log("Premium Content:", response);
    } catch (error) {
      // apiClient handles 401/403 redirects, but custom logic can go here
      console.error("Custom Error Handling for Premium Content Fetch:", error);
      // Optionally show a message to upgrade/subscribe if error status is 403
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    <div>
      <Button onClick={fetchPremiumContent} disabled={isLoading}>
        {isLoading ? "Loading Premium Content..." : "Fetch Premium Content"}
      </Button>
      {content && <pre>{JSON.stringify(content, null, 2)}</pre>}
    </div>
  );
}
 
export default PremiumContentFetcher;

Remember to adapt the database models (prisma/schema.prisma) and access checks (hasAccess field) based on your specific payment provider integration (Stripe or Lemon Squeezy) and how you manage user entitlements via webhooks.

On this page