API Calls
Learn how to make API calls with and without authentication in Indie Kit
Making API Calls 🔌
Learn how to build powerful features by making API calls in your Indie Kit application! We'll cover public and protected endpoints with practical examples. ✨
Understanding API Routes 🎯
Indie Kit uses Next.js App Router API routes. All API endpoints live in the src/app/api/
directory:
src/app/api/
products/route.ts ← Public endpoint
app/user-settings/route.ts ← Protected endpoint (requires auth)
Two Types of Endpoints
- Public endpoints: Anyone can access (e.g., blog posts, products)
- Protected endpoints: Require authentication (e.g., user settings, admin actions)
Public API Calls 🌐
Public endpoints don't require authentication. Use them for data that anyone can view.
Step 1: Create the API Endpoint
Create a new file at src/app/api/products/route.ts
:
// src/app/api/products/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
// In a real app, fetch from your database
const products = [
{ id: 1, name: 'Product 1', price: 99 },
{ id: 2, name: 'Product 2', price: 149 },
]
return NextResponse.json(products)
}
Step 2: Fetch Data in Your Component
Use SWR to fetch data with automatic caching and revalidation:
// src/app/products/page.tsx
'use client'
import useSWR from 'swr'
export default function ProductsPage() {
const { data, error, isLoading } = useSWR('/api/products')
if (error) return <div>Failed to load products</div>
if (isLoading) return <div>Loading...</div>
return (
<div>
<h1>Products</h1>
<ul>
{data.map((product) => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</div>
)
}
What is SWR?
SWR (Stale-While-Revalidate) is a data fetching library that comes pre-configured in Indie Kit. It provides automatic caching, revalidation, and loading states out of the box!
Protected API Calls 🔒
Protected endpoints require authentication. Use them for user-specific data or admin actions.
Important
Always use withAuthRequired
wrapper for protected endpoints. This ensures only authenticated users can access the data!
Step 1: Create Protected API Endpoint
Create a protected endpoint at src/app/api/app/user-settings/route.ts
:
// src/app/api/app/user-settings/route.ts
import { withAuthRequired } from '@/lib/auth/withAuthRequired'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import { users } from '@/db/schema'
import { eq } from 'drizzle-orm'
// GET user settings
export const GET = withAuthRequired(async (req, context) => {
const { session } = context
// Fetch settings from database
const userSettings = await db
.select()
.from(users)
.where(eq(users.id, session.user.id))
.limit(1)
return NextResponse.json({
userId: session.user.id,
theme: userSettings[0]?.theme || 'light',
notifications: userSettings[0]?.notifications || true,
})
})
// POST to update settings
export const POST = withAuthRequired(async (req, context) => {
const { session } = context
const body = await req.json()
// Update in database
await db
.update(users)
.set({ theme: body.theme })
.where(eq(users.id, session.user.id))
return NextResponse.json({ success: true })
})
Step 2: Fetch and Update Data
Use SWR for fetching and mutate
for optimistic updates:
// src/app/settings/page.tsx
'use client'
import useSWR, { mutate } from 'swr'
import { toast } from 'sonner'
export default function SettingsPage() {
const { data, error, isLoading } = useSWR('/api/app/user-settings')
const updateSettings = async (newSettings: { theme: string }) => {
try {
// Optimistically update the UI
mutate(
'/api/app/user-settings',
{ ...data, ...newSettings },
false
)
// Make the API call
const response = await fetch('/api/app/user-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newSettings),
})
if (!response.ok) throw new Error('Failed to update')
// Revalidate to get fresh data
mutate('/api/app/user-settings')
toast.success('Settings updated!')
} catch (error) {
// Rollback on error
mutate('/api/app/user-settings')
toast.error('Failed to update settings')
}
}
if (error) return <div>Failed to load settings</div>
if (isLoading) return <div>Loading...</div>
return (
<div>
<h1>User Settings</h1>
<div className="space-y-4">
<div>
<p>Current theme: {data.theme}</p>
<button
onClick={() => updateSettings({
theme: data.theme === 'light' ? 'dark' : 'light'
})}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Toggle Theme
</button>
</div>
</div>
</div>
)
}
SWR Features & Patterns 🎯
SWR comes pre-configured in Indie Kit with powerful features:
Basic Fetching
const { data, error, isLoading } = useSWR('/api/endpoint')
Type-Safe Fetching
interface Product {
id: number
name: string
price: number
}
const { data, error } = useSWR<Product[]>('/api/products')
Conditional Fetching
Only fetch when certain conditions are met:
// Only fetch when user is logged in
const { data } = useSWR(session ? '/api/app/profile' : null)
// Only fetch with valid ID
const { data } = useSWR(
productId ? `/api/products/${productId}` : null
)
Revalidation
import { mutate } from 'swr'
// Revalidate specific endpoint
mutate('/api/products')
// Revalidate all endpoints matching pattern
mutate((key) => typeof key === 'string' && key.startsWith('/api/products'))
Optimistic Updates
Update the UI immediately, then sync with server:
const addProduct = async (newProduct) => {
// Update UI optimistically
mutate('/api/products', [...data, newProduct], false)
// Make API call
await fetch('/api/products', {
method: 'POST',
body: JSON.stringify(newProduct)
})
// Revalidate to sync with server
mutate('/api/products')
}
Learn More
SWR has many more features! Check out the official documentation for pagination, infinite loading, and more.
Common Patterns 📚
Loading with Skeleton
import { Skeleton } from "@/components/ui/skeleton"
export default function ProductsPage() {
const { data, isLoading } = useSWR('/api/products')
if (isLoading) {
return (
<div className="space-y-4">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
</div>
)
}
return <ProductList products={data} />
}
Error Handling
export default function ProductsPage() {
const { data, error, isLoading } = useSWR('/api/products')
if (error) {
return (
<div className="p-4 bg-red-50 text-red-600 rounded">
Failed to load products. Please try again later.
</div>
)
}
if (isLoading) return <div>Loading...</div>
return <ProductList products={data} />
}
Mutation with Error Handling
const deleteProduct = async (id: number) => {
try {
// Optimistic update
const newData = data.filter(p => p.id !== id)
mutate('/api/products', newData, false)
// Make API call
const response = await fetch(`/api/products/${id}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('Delete failed')
toast.success('Product deleted!')
} catch (error) {
// Rollback on error
mutate('/api/products')
toast.error('Failed to delete product')
}
}
HTTP Methods 🔄
Use the appropriate HTTP method for each action:
GET - Fetch Data
export async function GET() {
const data = await fetchFromDatabase()
return NextResponse.json(data)
}
POST - Create New Resource
export async function POST(req: Request) {
const body = await req.json()
const newItem = await createInDatabase(body)
return NextResponse.json(newItem)
}
PUT - Update Entire Resource
export async function PUT(req: Request) {
const body = await req.json()
const updated = await updateInDatabase(body)
return NextResponse.json(updated)
}
PATCH - Partial Update
export async function PATCH(req: Request) {
const body = await req.json()
const patched = await partialUpdateInDatabase(body)
return NextResponse.json(patched)
}
DELETE - Remove Resource
export async function DELETE(req: Request) {
await deleteFromDatabase()
return NextResponse.json({ success: true })
}
Best Practices 💡
1. Always Handle Loading and Error States
const { data, error, isLoading } = useSWR('/api/endpoint')
if (error) return <ErrorMessage />
if (isLoading) return <LoadingSpinner />
return <DataDisplay data={data} />
2. Use TypeScript for Type Safety
interface User {
id: string
name: string
email: string
}
const { data } = useSWR<User>('/api/app/profile')
// data is now typed as User
3. Implement Optimistic Updates
Make your app feel instant:
// Update UI first
mutate('/api/todos', [...todos, newTodo], false)
// Then make API call
await createTodo(newTodo)
// Finally revalidate
mutate('/api/todos')
4. Validate Input Server-Side
export const POST = withAuthRequired(async (req, context) => {
const body = await req.json()
// Validate input
if (!body.name || body.name.length < 3) {
return NextResponse.json(
{ error: 'Name must be at least 3 characters' },
{ status: 400 }
)
}
// Process valid data
// ...
})
5. Use Proper Error Status Codes
// 400 - Bad Request (invalid input)
return NextResponse.json({ error: 'Invalid data' }, { status: 400 })
// 401 - Unauthorized (not logged in)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// 403 - Forbidden (logged in but no permission)
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
// 404 - Not Found
return NextResponse.json({ error: 'Not found' }, { status: 404 })
// 500 - Server Error
return NextResponse.json({ error: 'Server error' }, { status: 500 })
Security Checklist 🔐
- ✅ Use
withAuthRequired
for protected endpoints - ✅ Validate all input data server-side
- ✅ Never trust client-side data
- ✅ Use proper HTTP methods
- ✅ Return appropriate error codes
- ✅ Don't expose sensitive data in error messages
- ✅ Rate limit API endpoints if needed
- ✅ Use HTTPS in production
Troubleshooting 🔧
"useSWR is not defined"
Make sure your component has 'use client'
at the top:
'use client'
import useSWR from 'swr'
"401 Unauthorized" on Protected Routes
Ensure you're logged in and using withAuthRequired
:
export const GET = withAuthRequired(async (req, context) => {
// Your code here
})
Data Not Updating
Force revalidation after mutations:
await updateData()
mutate('/api/endpoint') // Add this
CORS Errors
Next.js API routes don't have CORS issues since they're same-origin. If calling external APIs, handle CORS server-side.
Next Steps 🚀
Now you know how to:
- ✅ Create public and protected API endpoints
- ✅ Fetch data with SWR
- ✅ Handle loading and error states
- ✅ Implement optimistic updates
- ✅ Use proper HTTP methods
- ✅ Secure your endpoints
Start Building
You're ready to build powerful features with API calls! Start with a simple endpoint and expand from there. 🎉