Overview
Next.js API routes allow you to create API endpoints within your application. They provide a powerful way to build RESTful APIs, handle webhooks, and integrate with external services.API Route Benefits
- Built-in HTTP handling - Automatic request/response parsing - Middleware support - TypeScript support
Use Cases
- RESTful APIs - Webhook endpoints - External API integrations - File uploads
Basic API Routes
Simple GET Route
Copy
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/database";
export async function GET() {
try {
const users = await db.users.findMany({
select: {
id: true,
name: true,
email: true,
createdAt: true,
},
});
return NextResponse.json({ success: true, data: users });
} catch (error) {
console.error("Error fetching users:", error);
return NextResponse.json(
{ success: false, error: "Failed to fetch users" },
{ status: 500 }
);
}
}
POST Route with Validation
Copy
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/database";
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
role: z.enum(["admin", "user", "viewer"]),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Validate input
const validationResult = userSchema.safeParse(body);
if (!validationResult.success) {
return NextResponse.json(
{
success: false,
error: "Validation failed",
details: validationResult.error.errors,
},
{ status: 400 }
);
}
// Check for existing user
const existingUser = await db.users.findUnique({
where: { email: body.email },
});
if (existingUser) {
return NextResponse.json(
{ success: false, error: "User with this email already exists" },
{ status: 409 }
);
}
const user = await db.users.create({
data: validationResult.data,
});
return NextResponse.json({ success: true, data: user }, { status: 201 });
} catch (error) {
console.error("Error creating user:", error);
return NextResponse.json(
{ success: false, error: "Failed to create user" },
{ status: 500 }
);
}
}
Dynamic Routes
Single Dynamic Parameter
Copy
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/database";
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const user = await db.users.findUnique({
where: { id: params.id },
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true,
updatedAt: true,
},
});
if (!user) {
return NextResponse.json(
{ success: false, error: "User not found" },
{ status: 404 }
);
}
return NextResponse.json({ success: true, data: user });
} catch (error) {
console.error("Error fetching user:", error);
return NextResponse.json(
{ success: false, error: "Failed to fetch user" },
{ status: 500 }
);
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json();
const user = await db.users.update({
where: { id: params.id },
data: body,
});
return NextResponse.json({ success: true, data: user });
} catch (error) {
console.error("Error updating user:", error);
return NextResponse.json(
{ success: false, error: "Failed to update user" },
{ status: 500 }
);
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
await db.users.delete({
where: { id: params.id },
});
return NextResponse.json({ success: true, message: "User deleted" });
} catch (error) {
console.error("Error deleting user:", error);
return NextResponse.json(
{ success: false, error: "Failed to delete user" },
{ status: 500 }
);
}
}
Multiple Dynamic Parameters
Copy
// app/api/posts/[userId]/[postId]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/database";
export async function GET(
request: NextRequest,
{ params }: { params: { userId: string; postId: string } }
) {
try {
const post = await db.posts.findFirst({
where: {
id: params.postId,
authorId: params.userId,
},
include: {
author: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
if (!post) {
return NextResponse.json(
{ success: false, error: "Post not found" },
{ status: 404 }
);
}
return NextResponse.json({ success: true, data: post });
} catch (error) {
console.error("Error fetching post:", error);
return NextResponse.json(
{ success: false, error: "Failed to fetch post" },
{ status: 500 }
);
}
}
Advanced API Patterns
Authentication Middleware
Copy
// app/api/middleware/auth.ts
import { NextRequest, NextResponse } from "next/server";
import jwt from "jsonwebtoken";
export function withAuth(handler: Function) {
return async (request: NextRequest, context: any) => {
try {
const token = request.headers
.get("authorization")
?.replace("Bearer ", "");
if (!token) {
return NextResponse.json(
{ success: false, error: "No token provided" },
{ status: 401 }
);
}
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
request.user = decoded;
return handler(request, context);
} catch (error) {
return NextResponse.json(
{ success: false, error: "Invalid token" },
{ status: 401 }
);
}
};
}
Using Authentication Middleware
Copy
// app/api/protected/route.ts
import { NextRequest, NextResponse } from "next/server";
import { withAuth } from "../middleware/auth";
async function handler(request: NextRequest) {
return NextResponse.json({
success: true,
message: "Protected route accessed",
user: request.user,
});
}
export const GET = withAuth(handler);
Rate Limiting
Copy
// app/api/middleware/rate-limit.ts
import { NextRequest, NextResponse } from "next/server";
const rateLimitMap = new Map();
export function withRateLimit(
handler: Function,
limit: number = 100,
windowMs: number = 60000
) {
return async (request: NextRequest, context: any) => {
const ip =
request.ip || request.headers.get("x-forwarded-for") || "unknown";
const now = Date.now();
const windowStart = now - windowMs;
// Clean old entries
for (const [key, value] of rateLimitMap.entries()) {
if (value.timestamp < windowStart) {
rateLimitMap.delete(key);
}
}
// Check rate limit
const userLimit = rateLimitMap.get(ip);
if (userLimit) {
if (userLimit.count >= limit) {
return NextResponse.json(
{ success: false, error: "Rate limit exceeded" },
{ status: 429 }
);
}
userLimit.count++;
} else {
rateLimitMap.set(ip, { count: 1, timestamp: now });
}
return handler(request, context);
};
}
File Upload Handling
Single File Upload
Copy
// app/api/upload/route.ts
import { NextRequest, NextResponse } from "next/server";
import { writeFile } from "fs/promises";
import { join } from "path";
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json(
{ success: false, error: "No file provided" },
{ status: 400 }
);
}
// Validate file type
const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
if (!allowedTypes.includes(file.type)) {
return NextResponse.json(
{ success: false, error: "Invalid file type" },
{ status: 400 }
);
}
// Validate file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
return NextResponse.json(
{ success: false, error: "File too large" },
{ status: 400 }
);
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const filename = `${Date.now()}-${file.name}`;
const path = join(process.cwd(), "public/uploads", filename);
await writeFile(path, buffer);
return NextResponse.json({
success: true,
filename,
url: `/uploads/${filename}`,
});
} catch (error) {
console.error("File upload error:", error);
return NextResponse.json(
{ success: false, error: "Failed to upload file" },
{ status: 500 }
);
}
}
Multiple File Upload
Copy
// app/api/upload/multiple/route.ts
import { NextRequest, NextResponse } from "next/server";
import { writeFile } from "fs/promises";
import { join } from "path";
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const files = formData.getAll("files") as File[];
if (!files || files.length === 0) {
return NextResponse.json(
{ success: false, error: "No files provided" },
{ status: 400 }
);
}
const uploadResults = [];
for (const file of files) {
// Validate file type
const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
if (!allowedTypes.includes(file.type)) {
uploadResults.push({
filename: file.name,
success: false,
error: "Invalid file type",
});
continue;
}
// Validate file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
uploadResults.push({
filename: file.name,
success: false,
error: "File too large",
});
continue;
}
try {
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const filename = `${Date.now()}-${file.name}`;
const path = join(process.cwd(), "public/uploads", filename);
await writeFile(path, buffer);
uploadResults.push({
filename: file.name,
success: true,
url: `/uploads/${filename}`,
});
} catch (error) {
uploadResults.push({
filename: file.name,
success: false,
error: "Upload failed",
});
}
}
return NextResponse.json({
success: true,
results: uploadResults,
});
} catch (error) {
console.error("Multiple file upload error:", error);
return NextResponse.json(
{ success: false, error: "Failed to upload files" },
{ status: 500 }
);
}
}
Webhook Handling
Stripe Webhook
Copy
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { db } from "@/lib/database";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: NextRequest) {
try {
const body = await request.text();
const signature = request.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (error) {
console.error("Webhook signature verification failed:", error);
return NextResponse.json(
{ success: false, error: "Invalid signature" },
{ status: 400 }
);
}
// Handle the event
switch (event.type) {
case "payment_intent.succeeded":
const paymentIntent = event.data.object as Stripe.PaymentIntent;
await handlePaymentSuccess(paymentIntent);
break;
case "payment_intent.payment_failed":
const failedPayment = event.data.object as Stripe.PaymentIntent;
await handlePaymentFailure(failedPayment);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Webhook error:", error);
return NextResponse.json(
{ success: false, error: "Webhook processing failed" },
{ status: 500 }
);
}
}
async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) {
// Update order status in database
await db.orders.update({
where: { paymentIntentId: paymentIntent.id },
data: { status: "paid" },
});
}
async function handlePaymentFailure(paymentIntent: Stripe.PaymentIntent) {
// Update order status in database
await db.orders.update({
where: { paymentIntentId: paymentIntent.id },
data: { status: "failed" },
});
}
Error Handling and Logging
Comprehensive Error Handling
Copy
// app/api/error-handling/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/database";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get("limit") || "10");
// Validate parameters
if (page < 1 || limit < 1 || limit > 100) {
return NextResponse.json(
{
success: false,
error: "Invalid parameters",
details: "Page must be >= 1, limit must be between 1 and 100",
},
{ status: 400 }
);
}
const skip = (page - 1) * limit;
const [items, total] = await Promise.all([
db.items.findMany({
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
db.items.count(),
]);
return NextResponse.json({
success: true,
data: items,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error("API Error:", error);
// Log error details for debugging
if (process.env.NODE_ENV === "development") {
console.error("Error details:", error);
}
return NextResponse.json(
{ success: false, error: "Internal server error" },
{ status: 500 }
);
}
}
Best Practices
API Route Best Practices
1
Input Validation
Always validate and sanitize input data
2
Error Handling
Provide comprehensive error handling and logging
3
HTTP Status Codes
Use appropriate HTTP status codes for responses
4
Security
Implement authentication, authorization, and rate limiting
Performance Optimization
Database Optimization
- Use proper indexing - Implement connection pooling - Use database transactions - Optimize queries
Response Optimization
- Implement caching - Use compression - Optimize JSON responses - Implement pagination
Common Patterns
CRUD API
Copy
// app/api/items/route.ts
export async function GET() {
// Read all items
}
export async function POST(request: NextRequest) {
// Create new item
}
// app/api/items/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
// Read single item
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
// Update item
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
// Delete item
}
Search API
Copy
// app/api/search/route.ts
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const query = searchParams.get("q");
const category = searchParams.get("category");
const page = parseInt(searchParams.get("page") || "1");
// Implement search logic
}
Troubleshooting
API Route Not Found
API Route Not Found
Problem: 404 error when accessing API route
Solution: Check file naming convention and ensure route is in the correct directory
CORS Issues
CORS Issues
Problem: CORS errors when calling API from frontend Solution:
Configure CORS headers or use Next.js built-in CORS handling
Request Body Issues
Request Body Issues
Problem: Request body is empty or malformed Solution: Ensure proper
Content-Type headers and body parsing
Database Connection Issues
Database Connection Issues
Problem: Database operations failing in API routes
Solution: Check database connection and implement proper error handling
Key Takeaway: API routes provide a powerful way to build RESTful APIs
within your Next.js application. Use them for data operations, webhooks, and
external integrations while following best practices for security, validation,
and error handling.
Overview
Server Actions are a Next.js feature that allows you to run server-side code directly from client components. They provide a secure, type-safe way to handle form submissions and other server operations.Server Action Benefits
- Type-safe server operations - Automatic serialization - Built-in error handling - Form integration
Use Cases
- Form submissions - Database mutations - File uploads - API integrations
Basic Server Actions
Simple Server Action
Copy
// app/actions/user-actions.ts
"use server";
import { db } from "@/lib/database";
import { revalidatePath } from "next/cache";
export async function createUser(formData: FormData) {
try {
const user = await db.users.create({
data: {
name: formData.get("name") as string,
email: formData.get("email") as string,
role: formData.get("role") as string,
},
});
revalidatePath("/users");
return { success: true, user };
} catch (error) {
return { success: false, error: "Failed to create user" };
}
}
Using Server Actions in Forms
Copy
// app/components/UserForm.tsx
import { createUser } from "@/app/actions/user-actions";
export default function UserForm() {
return (
<form action={createUser} className="space-y-4">
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
required
className="w-full p-2 border rounded"
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
required
className="w-full p-2 border rounded"
/>
</div>
<div>
<label htmlFor="role">Role:</label>
<select id="role" name="role" className="w-full p-2 border rounded">
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="viewer">Viewer</option>
</select>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600">
Create User
</button>
</form>
);
}
Advanced Server Actions
Server Action with Validation
Copy
// app/actions/user-actions.ts
"use server";
import { db } from "@/lib/database";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
role: z.enum(["admin", "user", "viewer"]),
});
export async function createUser(formData: FormData) {
try {
const data = {
name: formData.get("name") as string,
email: formData.get("email") as string,
role: formData.get("role") as string,
};
// Validate input
const validationResult = userSchema.safeParse(data);
if (!validationResult.success) {
return {
success: false,
error: "Validation failed",
details: validationResult.error.errors,
};
}
// Check for existing user
const existingUser = await db.users.findUnique({
where: { email: data.email },
});
if (existingUser) {
return {
success: false,
error: "User with this email already exists",
};
}
const user = await db.users.create({
data: validationResult.data,
});
revalidatePath("/users");
return { success: true, user };
} catch (error) {
console.error("User creation error:", error);
return { success: false, error: "Failed to create user" };
}
}
Server Action with File Upload
Copy
// app/actions/file-actions.ts
"use server";
import { writeFile } from "fs/promises";
import { join } from "path";
import { revalidatePath } from "next/cache";
export async function uploadFile(formData: FormData) {
try {
const file = formData.get("file") as File;
if (!file) {
return { success: false, error: "No file provided" };
}
// Validate file type
const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
if (!allowedTypes.includes(file.type)) {
return { success: false, error: "Invalid file type" };
}
// Validate file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
return { success: false, error: "File too large" };
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const filename = `${Date.now()}-${file.name}`;
const path = join(process.cwd(), "public/uploads", filename);
await writeFile(path, buffer);
revalidatePath("/uploads");
return { success: true, filename };
} catch (error) {
console.error("File upload error:", error);
return { success: false, error: "Failed to upload file" };
}
}
Form Handling Patterns
Progressive Enhancement
Copy
// app/components/ContactForm.tsx
"use client";
import { useState } from "react";
import { submitContactForm } from "@/app/actions/contact-actions";
export default function ContactForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState("");
const handleSubmit = async (formData: FormData) => {
setIsSubmitting(true);
setMessage("");
try {
const result = await submitContactForm(formData);
if (result.success) {
setMessage("Message sent successfully!");
// Reset form
const form = document.getElementById("contact-form") as HTMLFormElement;
form?.reset();
} else {
setMessage(result.error || "Failed to send message");
}
} catch (error) {
setMessage("An error occurred. Please try again.");
} finally {
setIsSubmitting(false);
}
};
return (
<form id="contact-form" action={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
required
className="w-full p-2 border rounded"
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
required
className="w-full p-2 border rounded"
/>
</div>
<div>
<label htmlFor="message">Message:</label>
<textarea
id="message"
name="message"
required
rows={4}
className="w-full p-2 border rounded"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600 disabled:opacity-50">
{isSubmitting ? "Sending..." : "Send Message"}
</button>
{message && (
<div
className={`p-2 rounded ${
message.includes("success")
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}>
{message}
</div>
)}
</form>
);
}
Multi-Step Forms
Copy
// app/actions/multi-step-actions.ts
"use server";
import { db } from "@/lib/database";
import { revalidatePath } from "next/cache";
export async function saveStep1(formData: FormData) {
try {
const data = {
name: formData.get("name") as string,
email: formData.get("email") as string,
};
// Save to temporary storage or session
// This is a simplified example
return { success: true, data };
} catch (error) {
return { success: false, error: "Failed to save step 1" };
}
}
export async function saveStep2(formData: FormData) {
try {
const data = {
company: formData.get("company") as string,
position: formData.get("position") as string,
};
return { success: true, data };
} catch (error) {
return { success: false, error: "Failed to save step 2" };
}
}
export async function completeRegistration(formData: FormData) {
try {
const userData = {
name: formData.get("name") as string,
email: formData.get("email") as string,
company: formData.get("company") as string,
position: formData.get("position") as string,
};
const user = await db.users.create({
data: userData,
});
revalidatePath("/users");
return { success: true, user };
} catch (error) {
return { success: false, error: "Failed to complete registration" };
}
}
Error Handling
Comprehensive Error Handling
Copy
// app/actions/error-handling-actions.ts
"use server";
import { db } from "@/lib/database";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
try {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
const authorId = formData.get("authorId") as string;
// Validate required fields
if (!title || !content || !authorId) {
return {
success: false,
error: "All fields are required",
field: !title ? "title" : !content ? "content" : "authorId",
};
}
// Check if author exists
const author = await db.users.findUnique({
where: { id: authorId },
});
if (!author) {
return {
success: false,
error: "Author not found",
field: "authorId",
};
}
// Create post
const post = await db.posts.create({
data: {
title,
content,
authorId,
publishedAt: new Date(),
},
});
revalidatePath("/posts");
return { success: true, post };
} catch (error) {
console.error("Post creation error:", error);
// Handle specific database errors
if (error instanceof Error) {
if (error.message.includes("Unique constraint")) {
return {
success: false,
error: "A post with this title already exists",
field: "title",
};
}
}
return {
success: false,
error: "Failed to create post. Please try again.",
};
}
}
Real-World Examples
E-commerce Product Management
Copy
// app/actions/product-actions.ts
"use server";
import { db } from "@/lib/database";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const productSchema = z.object({
name: z.string().min(1, "Product name is required"),
description: z.string().min(10, "Description must be at least 10 characters"),
price: z.number().positive("Price must be positive"),
categoryId: z.string().min(1, "Category is required"),
stock: z.number().int().min(0, "Stock cannot be negative"),
});
export async function createProduct(formData: FormData) {
try {
const data = {
name: formData.get("name") as string,
description: formData.get("description") as string,
price: parseFloat(formData.get("price") as string),
categoryId: formData.get("categoryId") as string,
stock: parseInt(formData.get("stock") as string),
};
// Validate input
const validationResult = productSchema.safeParse(data);
if (!validationResult.success) {
return {
success: false,
error: "Validation failed",
details: validationResult.error.errors,
};
}
// Check if category exists
const category = await db.categories.findUnique({
where: { id: data.categoryId },
});
if (!category) {
return {
success: false,
error: "Category not found",
field: "categoryId",
};
}
// Create product
const product = await db.products.create({
data: validationResult.data,
include: {
category: true,
},
});
revalidatePath("/products");
return { success: true, product };
} catch (error) {
console.error("Product creation error:", error);
return { success: false, error: "Failed to create product" };
}
}
export async function updateProduct(id: string, formData: FormData) {
try {
const data = {
name: formData.get("name") as string,
description: formData.get("description") as string,
price: parseFloat(formData.get("price") as string),
categoryId: formData.get("categoryId") as string,
stock: parseInt(formData.get("stock") as string),
};
const validationResult = productSchema.safeParse(data);
if (!validationResult.success) {
return {
success: false,
error: "Validation failed",
details: validationResult.error.errors,
};
}
const product = await db.products.update({
where: { id },
data: validationResult.data,
include: {
category: true,
},
});
revalidatePath("/products");
revalidatePath(`/products/${id}`);
return { success: true, product };
} catch (error) {
console.error("Product update error:", error);
return { success: false, error: "Failed to update product" };
}
}
export async function deleteProduct(id: string) {
try {
await db.products.delete({
where: { id },
});
revalidatePath("/products");
return { success: true };
} catch (error) {
console.error("Product deletion error:", error);
return { success: false, error: "Failed to delete product" };
}
}
Best Practices
Server Action Best Practices
1
Input Validation
Always validate input data using schemas
2
Error Handling
Provide comprehensive error handling and user feedback
3
Revalidation
Use revalidatePath to update cached data
4
Security
Implement proper authentication and authorization
Performance Considerations
Optimization Tips
- Use database transactions for multiple operations - Implement proper indexing - Cache frequently accessed data - Use connection pooling
Security Tips
- Validate all inputs - Sanitize data before database operations - Implement rate limiting - Use proper authentication
Common Patterns
CRUD Operations
Copy
// app/actions/crud-actions.ts
"use server";
import { db } from "@/lib/database";
import { revalidatePath } from "next/cache";
export async function createItem(formData: FormData) {
// Create logic
}
export async function readItem(id: string) {
// Read logic
}
export async function updateItem(id: string, formData: FormData) {
// Update logic
}
export async function deleteItem(id: string) {
// Delete logic
}
Batch Operations
Copy
// app/actions/batch-actions.ts
"use server";
import { db } from "@/lib/database";
import { revalidatePath } from "next/cache";
export async function bulkUpdateItems(formData: FormData) {
try {
const items = JSON.parse(formData.get("items") as string);
await db.$transaction(
items.map((item: any) =>
db.items.update({
where: { id: item.id },
data: item.data,
})
)
);
revalidatePath("/items");
return { success: true };
} catch (error) {
return { success: false, error: "Failed to update items" };
}
}
Troubleshooting
Server Action Not Working
Server Action Not Working
Problem: Server action not executing or returning errors
Solution: Check that the action has ‘use server’ directive and is properly imported
Form Data Not Received
Form Data Not Received
Problem: FormData is empty or missing fields Solution: Ensure form
inputs have proper name attributes and the form is submitted correctly
Validation Errors
Validation Errors
Problem: Validation is failing unexpectedly Solution: Check validation
schema and ensure data types match expected formats
Database Errors
Database Errors
Problem: Database operations failing in server actions
Solution: Verify database connection and check for proper error handling
Key Takeaway: Server Actions provide a powerful way to handle server-side
operations with type safety and automatic serialization. Use them for form
submissions, database operations, and other server-side tasks while
maintaining good error handling and validation practices.