Skip to main content
Glama
app.tsx6.71 kB
// Next.js 14 App Router - Server & Client Components // ============================================ // Server Component (Default) - layout.tsx // ============================================ import { Metadata } from "next"; export const metadata: Metadata = { title: "My App", description: "Built with Next.js 14", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body className="min-h-screen bg-gray-50"> <nav className="bg-white shadow-sm"> <div className="max-w-7xl mx-auto px-4 py-3"> <a href="/" className="text-xl font-bold text-gray-900"> MyApp </a> </div> </nav> <main className="max-w-7xl mx-auto px-4 py-8">{children}</main> </body> </html> ); } // ============================================ // Server Component with Data Fetching - page.tsx // ============================================ interface Post { id: number; title: string; body: string; } async function getPosts(): Promise<Post[]> { const res = await fetch( "https://jsonplaceholder.typicode.com/posts?_limit=10", { next: { revalidate: 60 }, // Revalidate every 60 seconds }, ); if (!res.ok) throw new Error("Failed to fetch posts"); return res.json(); } export default async function HomePage() { const posts = await getPosts(); return ( <div className="space-y-6"> <h1 className="text-3xl font-bold">Latest Posts</h1> <div className="grid gap-4 md:grid-cols-2"> {posts.map((post) => ( <article key={post.id} className="p-6 bg-white rounded-lg shadow"> <h2 className="text-xl font-semibold mb-2">{post.title}</h2> <p className="text-gray-600">{post.body}</p> </article> ))} </div> </div> ); } // ============================================ // Client Component - Counter.tsx // ============================================ ("use client"); import { useState } from "react"; export function Counter() { const [count, setCount] = useState(0); return ( <div className="flex items-center gap-4"> <button onClick={() => setCount((c) => c - 1)} className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300" > - </button> <span className="text-2xl font-bold">{count}</span> <button onClick={() => setCount((c) => c + 1)} className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" > + </button> </div> ); } // ============================================ // Server Action - actions.ts // ============================================ ("use server"); import { revalidatePath } from "next/cache"; interface FormState { success: boolean; message: string; } export async function createPost( prevState: FormState, formData: FormData, ): Promise<FormState> { const title = formData.get("title") as string; const body = formData.get("body") as string; // Validate if (!title || title.length < 3) { return { success: false, message: "Title must be at least 3 characters" }; } // Simulate API call await new Promise((resolve) => setTimeout(resolve, 1000)); // In real app: save to database console.log("Creating post:", { title, body }); // Revalidate the posts page revalidatePath("/"); return { success: true, message: "Post created successfully!" }; } // ============================================ // Form with Server Action - CreatePostForm.tsx // ============================================ ("use client"); import { useFormState, useFormStatus } from "react-dom"; import { createPost } from "./actions"; function SubmitButton() { const { pending } = useFormStatus(); return ( <button type="submit" disabled={pending} className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50" > {pending ? "Creating..." : "Create Post"} </button> ); } export function CreatePostForm() { const [state, action] = useFormState(createPost, { success: false, message: "", }); return ( <form action={action} className="space-y-4 max-w-md"> {state.message && ( <div className={`p-3 rounded ${state.success ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}`} > {state.message} </div> )} <div> <label htmlFor="title" className="block text-sm font-medium mb-1"> Title </label> <input type="text" id="title" name="title" required className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" /> </div> <div> <label htmlFor="body" className="block text-sm font-medium mb-1"> Content </label> <textarea id="body" name="body" rows={4} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" /> </div> <SubmitButton /> </form> ); } // ============================================ // API Route - app/api/posts/route.ts // ============================================ import { NextRequest, NextResponse } from "next/server"; const posts: Post[] = []; export async function GET() { return NextResponse.json(posts); } export async function POST(request: NextRequest) { const body = await request.json(); if (!body.title) { return NextResponse.json({ error: "Title required" }, { status: 400 }); } const post: Post = { id: posts.length + 1, title: body.title, body: body.body || "", }; posts.push(post); return NextResponse.json(post, { status: 201 }); } // ============================================ // Middleware - middleware.ts // ============================================ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export function middleware(request: NextRequest) { // Add timing header const response = NextResponse.next(); response.headers.set("x-middleware-timestamp", Date.now().toString()); // Redirect logic example if (request.nextUrl.pathname === "/old-page") { return NextResponse.redirect(new URL("/new-page", request.url)); } // Auth check example const token = request.cookies.get("token"); if (request.nextUrl.pathname.startsWith("/dashboard") && !token) { return NextResponse.redirect(new URL("/login", request.url)); } return response; } export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/millsydotdev/Code-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server