Skip to main content
Glama
app.tsx7.28 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