Module 11 — Server Components
Server Components are one of the most important concepts in modern Next.js development.
If you understand Server Components correctly, you can build applications that:
- Send less JavaScript to the browser
- Fetch data close to the source
- Keep sensitive server logic away from users
- Access databases from the server
- Keep API credentials private
- Separate server logic from browser interactivity
- Build more scalable application architectures
The most important thing to understand is that Server Components are not simply another type of React component.
They change where your component code executes and how your application is architected.
What Is a Server Component?
A Server Component is a React component that executes on the server as part of the application's rendering process.
In the Next.js App Router, components are Server Components by default.
For example:
1export default function CoursePage() { 2 return ( 3 <main> 4 <h1>Next.js Roadmap</h1> 5 <p>Learn Next.js from beginner to advanced.</p> 6 </main> 7 ); 8}
There is no:
1"use client";
directive.
Therefore, this component can remain a Server Component.
A simplified architecture looks like this:
1 Next.js Server 2 │ 3 ↓ 4 Server Component 5 │ 6 ┌────────────┴────────────┐ 7 ↓ ↓ 8 Fetch Data Render UI 9 │ │ 10 └────────────┬────────────┘ 11 ↓ 12 Browser
The browser does not need to execute the component's server-side logic.
Why Server Components Matter
Consider a course page.
The page needs:
- Course title
- Description
- Instructor
- Lessons
- Course duration
- Database information
A traditional client-heavy approach might look like:
1Browser 2 ↓ 3Download JavaScript 4 ↓ 5Run React 6 ↓ 7useEffect() 8 ↓ 9API request 10 ↓ 11Database/API 12 ↓ 13Response 14 ↓ 15Update state 16 ↓ 17Render
There is a lot of work happening in the browser.
With a Server Component, the architecture can be closer to:
1Browser 2 ↑ 3 │ Rendered result 4 │ 5Next.js Server 6 ↓ 7Server Component 8 ↓ 9Database/API
The server can perform the data-fetching work before the UI is delivered to the browser.
This can be especially useful for content-heavy pages such as:
- Course pages
- Documentation
- Blogs
- Product pages
- Dashboards
- Learning platforms
Server Components Are the Default
With the App Router, this is a Server Component:
1export default function HomePage() { 2 return ( 3 <main> 4 <h1>Welcome to Tech3Space</h1> 5 </main> 6 ); 7}
You do not need to write:
1"use server";
to make a normal component a Server Component.
This distinction is important.
"use client"
Marks a module as a Client Component boundary.
"use server"
Is used for server-side function/action semantics and is not the general way to declare ordinary Server Components.
Therefore, do not confuse:
1"use client";
with:
1"use server";
Server Component vs Client Component
A useful mental model is:
1 Next.js Application 2 │ 3 ┌────────────┴────────────┐ 4 ↓ ↓ 5 Server Components Client Components 6 │ │ 7 Server execution Browser execution 8 │ │ 9 Database access useState 10 Server APIs useEffect 11 Secrets Events 12 Server-only packages Browser APIs 13 │ │ 14 └────────────┬────────────┘ 15 ↓ 16 UI
Server Components are primarily useful for server-side work.
Client Components are required when you need browser-side interactivity.
When Should You Use a Server Component?
Use a Server Component when the component primarily needs to:
Fetch data
1Database 2API 3CMS 4Filesystem
Access server-only resources
1Database credentials 2Private API keys 3Server environment variables
Render static or server-generated UI
Examples:
- Blog article
- Documentation page
- Course description
- Product details
Reduce client-side JavaScript
If a component does not need browser interaction, keeping it on the server can simplify the client bundle.
When Should You Use a Client Component?
A Client Component is appropriate when you need browser-side behavior.
Examples:
useStateuseEffectuseReduceruseRefonClickonChange- Browser APIs
- Interactive animations
- Client-side event handling
For example:
1"use client"; 2 3import { useState } from "react"; 4 5export default function CourseProgress() { 6 const [completed, setCompleted] = useState(false); 7 8 return ( 9 <button onClick={() => setCompleted(!completed)}> 10 {completed ? "Completed" : "Mark as complete"} 11 </button> 12 ); 13}
This needs to run in the browser because the user is interacting with it.
Real-Life Example: Course Platform
Imagine a Tech3Space course page.
The page contains:
1Course 2├── Title 3├── Description 4├── Instructor 5├── Lessons 6├── Duration 7├── Course Content 8└── Mark as Complete button
Not everything needs to be a Client Component.
A better architecture is:
1CoursePage 2│ 3├── CourseInformation Server Component 4│ 5├── LessonList Server Component 6│ 7├── InstructorInformation Server Component 8│ 9└── ProgressButton Client Component
This is an important architectural pattern.
Do not convert the entire page into a Client Component just because one button needs interactivity.
Server Component with Data Fetching
One of the most useful features of Server Components is server-side data fetching.
Consider a course page:
1type Course = { 2 id: string; 3 title: string; 4 description: string; 5}; 6 7async function getCourse(): Promise<Course> { 8 const response = await fetch( 9 "https://api.example.com/courses/nextjs" 10 ); 11 12 if (!response.ok) { 13 throw new Error("Failed to fetch course"); 14 } 15 16 return response.json(); 17} 18 19export default async function CoursePage() { 20 const course = await getCourse(); 21 22 return ( 23 <main> 24 <h1>{course.title}</h1> 25 <p>{course.description}</p> 26 </main> 27 ); 28}
Notice something important:
1export default async function CoursePage()
The component can be asynchronous.
It can perform server-side data fetching without requiring:
1useEffect()
and:
1useState()
for the initial server-rendered data.
Why Not Fetch Everything with useEffect?
A common beginner pattern is:
1"use client"; 2 3import { useEffect, useState } from "react"; 4 5export default function Courses() { 6 const [courses, setCourses] = useState([]); 7 8 useEffect(() => { 9 fetch("/api/courses") 10 .then((response) => response.json()) 11 .then(setCourses); 12 }, []); 13 14 return <div>...</div>; 15}
This can be appropriate in some situations, but it should not automatically be your default approach.
The flow is:
1Browser 2 ↓ 3Download Client JavaScript 4 ↓ 5React starts 6 ↓ 7useEffect() 8 ↓ 9API request 10 ↓ 11Data arrives 12 ↓ 13State update 14 ↓ 15Render
For initial page data, a Server Component can often simplify this:
1export default async function Courses() { 2 const courses = await getCourses(); 3 4 return ( 5 <main> 6 {courses.map((course) => ( 7 <article key={course.id}> 8 <h2>{course.title}</h2> 9 </article> 10 ))} 11 </main> 12 ); 13}
The important lesson is:
Use client-side fetching when the interaction actually requires it; don't use
useEffectsimply because you are fetching data.
Server Components and Database Access
One of the most powerful applications of Server Components is accessing server-side data sources.
For example:
1Next.js Server Component 2 │ 3 ↓ 4 Database 5 │ 6 ↓ 7 Courses 8 │ 9 ↓ 10 Render
Conceptually:
1import { db } from "@/lib/db"; 2 3export default async function CoursesPage() { 4 const courses = await db.course.findMany(); 5 6 return ( 7 <main> 8 <h1>Courses</h1> 9 10 {courses.map((course) => ( 11 <article key={course.id}> 12 <h2>{course.title}</h2> 13 <p>{course.description}</p> 14 </article> 15 ))} 16 </main> 17 ); 18}
The database connection remains on the server.
The browser does not receive the database credentials.
Why This Is Better for Security
Imagine you have:
1DATABASE_URL 2PRIVATE_API_KEY 3STRIPE_SECRET_KEY 4JWT_SECRET
These values must never be exposed to browser JavaScript.
A Server Component can use server-only configuration:
1import { db } from "@/lib/db"; 2 3export default async function AdminCourses() { 4 const courses = await db.course.findMany(); 5 6 return <CourseList courses={courses} />; 7}
The database access happens on the server.
The browser receives the resulting UI/data needed for the page, not your database connection itself.
Environment Variables
Next.js applications commonly use environment variables for configuration.
For example:
1DATABASE_URL="postgresql://..." 2PRIVATE_API_KEY="secret-value" 3NEXT_PUBLIC_API_URL="https://example.com"
A critical distinction is:
1DATABASE_URL
versus:
1NEXT_PUBLIC_API_URL
Variables beginning with:
1NEXT_PUBLIC_
are intended to be available to client-side code.
Sensitive secrets should not be exposed through public environment variables.
Good
1DATABASE_URL=... 2STRIPE_SECRET_KEY=... 3PRIVATE_API_KEY=...
Public
1NEXT_PUBLIC_API_URL=... 2NEXT_PUBLIC_SITE_URL=...
Never assume that putting a secret into .env automatically makes it safe.
The important question is:
Can this value reach browser-executed code?
Server-Only Code
Some modules should never be imported into Client Components.
Examples:
1Database clients 2Private API clients 3Secret management 4Filesystem operations 5Server credentials
You can make the intention explicit with server-only utilities where appropriate.
For example, a data-access layer might look like:
1// lib/courses.ts 2 3import "server-only"; 4 5import { db } from "@/lib/db"; 6 7export async function getCourses() { 8 return db.course.findMany(); 9}
Then a Server Component can use it:
1import { getCourses } from "@/lib/courses"; 2 3export default async function CoursesPage() { 4 const courses = await getCourses(); 5 6 return ( 7 <main> 8 {courses.map((course) => ( 9 <h2 key={course.id}>{course.title}</h2> 10 ))} 11 </main> 12 ); 13}
The server-only package helps prevent accidental use of server-only modules from the client side.
Server Components and API Calls
Server Components can call external APIs directly.
For example:
1async function getProducts() { 2 const response = await fetch( 3 "https://api.example.com/products" 4 ); 5 6 if (!response.ok) { 7 throw new Error("Unable to load products"); 8 } 9 10 return response.json(); 11} 12 13export default async function ProductsPage() { 14 const products = await getProducts(); 15 16 return ( 17 <main> 18 {products.map((product: any) => ( 19 <article key={product.id}> 20 <h2>{product.name}</h2> 21 </article> 22 ))} 23 </main> 24 ); 25}
This is different from fetching the API from the browser.
The request can happen on the server.
Next.js with a Separate Backend
Suppose your architecture is:
1 Browser 2 │ 3 ↓ 4 Next.js 5 │ 6 ↓ 7 Django API 8 │ 9 ↓ 10 PostgreSQL
A Server Component can retrieve data from your backend:
1async function getCourses() { 2 const response = await fetch( 3 `${process.env.API_URL}/api/courses/` 4 ); 5 6 if (!response.ok) { 7 throw new Error("Failed to fetch courses"); 8 } 9 10 return response.json(); 11} 12 13export default async function CoursesPage() { 14 const courses = await getCourses(); 15 16 return ( 17 <main> 18 <h1>Courses</h1> 19 20 {courses.map((course: any) => ( 21 <article key={course.id}> 22 <h2>{course.title}</h2> 23 </article> 24 ))} 25 </main> 26 ); 27}
This is a common architecture when Next.js acts as the web application layer while Django, Node.js, or another backend handles business logic and APIs.
Server and Client Component Boundaries
Understanding the boundary between Server and Client Components is critical.
Consider:
1CoursePage 2│ 3├── CourseHeader 4├── CourseDescription 5├── LessonList 6└── ProgressButton
The architecture can be:
1CoursePage Server 2│ 3├── CourseHeader Server 4├── CourseDescription Server 5├── LessonList Server 6└── ProgressButton Client
This is usually better than:
1CoursePage Client 2│ 3├── CourseHeader 4├── CourseDescription 5├── LessonList 6└── ProgressButton
when only ProgressButton needs client-side interactivity.
Passing Data from Server to Client
A Server Component can render a Client Component and pass serializable data as props.
For example:
1import ProgressButton from "./ProgressButton"; 2 3export default async function LessonPage() { 4 const lesson = await getLesson(); 5 6 return ( 7 <main> 8 <h1>{lesson.title}</h1> 9 10 <ProgressButton lessonId={lesson.id} /> 11 </main> 12 ); 13}
The Client Component:
1"use client"; 2 3import { useState } from "react"; 4 5export default function ProgressButton({ 6 lessonId, 7}: { 8 lessonId: string; 9}) { 10 const [completed, setCompleted] = useState(false); 11 12 return ( 13 <button onClick={() => setCompleted(true)}> 14 {completed ? "Completed" : "Complete Lesson"} 15 </button> 16 ); 17}
The server handles the initial data.
The client handles interaction.
A Real-World Course Lesson Architecture
A sophisticated lesson page could be structured like this:
1LessonPage 2│ 3├── CourseNavigation Server 4│ 5├── LessonContent Server 6│ 7├── CodeExamples Server 8│ 9├── RelatedLessons Server 10│ 11├── ProgressButton Client 12│ 13├── LessonRating Client 14│ 15└── CommentForm Client
This architecture keeps the majority of the page server-rendered while isolating interactive features.
Server Components and Authentication
Server Components are also useful for authenticated pages.
For example:
1export default async function DashboardPage() { 2 const user = await getCurrentUser(); 3 4 if (!user) { 5 redirect("/login"); 6 } 7 8 return ( 9 <main> 10 <h1>Welcome, {user.name}</h1> 11 </main> 12 ); 13}
The server can determine the current user before rendering the page.
However, authentication architecture should be implemented carefully.
Do not treat rendering a different UI as the only security boundary.
Authorization must also be checked when sensitive operations are performed.
Authentication vs Authorization
These concepts are different.
Authentication
Answers:
Who are you?
Example:
1User → Login → Session
Authorization
Answers:
What are you allowed to access?
Example:
1User 2 │ 3 ├── View courses ✓ 4 ├── Edit own profile ✓ 5 ├── Delete users ✗ 6 └── Admin dashboard ✗
A production application needs both.
Protecting Server-Side Operations
Suppose an endpoint allows deleting a course.
It is not enough to hide the Delete button.
Bad security model:
1Hide Delete Button 2 ↓ 3Assume user cannot delete
Better:
1Request 2 ↓ 3Authenticate user 4 ↓ 5Check authorization 6 ↓ 7Validate input 8 ↓ 9Perform operation
The security decision must happen on the trusted server side.
Server Components and Performance
Server Components can help reduce unnecessary client-side JavaScript.
Consider a documentation page containing:
1Title 2Introduction 320 paragraphs 410 code blocks 5Related articles 6Footer
If none of those sections require interaction, turning the entire page into a Client Component can be unnecessary.
Instead:
1DocumentationPage 2 ↓ 3Server Component 4 ↓ 5Static/server-rendered content
Then an interactive search box can be isolated:
1DocumentationPage 2│ 3├── DocumentationContent Server 4└── SearchBox Client
This creates a smaller client-side boundary.
Server Components Are Not a Replacement for Client Components
A common misconception is:
"Server Components are better, so I should never use Client Components."
That is incorrect.
Modern applications normally use both.
For example:
1 Application 2 │ 3 ┌──────────┴──────────┐ 4 ↓ ↓ 5 Server Client 6 │ │ 7 Data fetching Interactions 8 Database Forms 9 SEO State 10 Server logic Browser APIs 11 │ │ 12 └──────────┬──────────┘ 13 ↓ 14 UI
The goal is to establish a sensible boundary.
Common Server Component Mistakes
Mistake 1 — Adding "use client" Everywhere
Avoid this:
1"use client"; 2 3export default function Article() { 4 return <article>...</article>; 5}
if the component has no client-side behavior.
Mistake 2 — Using Browser APIs in Server Components
This will not work as expected:
1export default function Page() { 2 const width = window.innerWidth; 3 4 return <p>{width}</p>; 5}
window is a browser API.
If browser APIs are required, use a Client Component.
Mistake 3 — Using Hooks in Server Components
Hooks such as:
1useState 2useEffect 3useReducer
are client-side React features in this context.
If your component needs them, it generally needs to be a Client Component.
Mistake 4 — Exposing Secrets
Never move private credentials into client-executed code.
Bad:
1"use client"; 2 3const secret = process.env.PRIVATE_API_KEY;
Do not design your application around exposing secrets to the browser.
Keep secret operations on the server.
Mistake 5 — Making the Entire Page Client-Side
If only one small component requires interaction, isolate that component.
Prefer:
1Page Server 2├── Content Server 3├── Data Server 4└── InteractiveButton Client
over unnecessarily converting the entire page into a Client Component.
Server Component Decision Checklist
Before adding:
1"use client";
ask:
1Does this component use useState? 2 │ 3 ├── Yes → Client Component 4 │ 5 └── No 6 ↓ 7Does it use browser APIs? 8 │ 9 ├── Yes → Client Component 10 │ 11 └── No 12 ↓ 13Does it need event handlers? 14 │ 15 ├── Yes → Client Component 16 │ 17 └── No 18 ↓ 19Can it remain on the server? 20 │ 21 └── Yes → Server Component
This simple decision process prevents many architectural mistakes.
Practical Project: Build a Course Page
Let's combine the concepts.
Step 1 — Create the Route
1app/ 2└── courses/ 3 └── [slug]/ 4 └── page.tsx
Step 2 — Create the Data Layer
1// lib/courses.ts 2 3import "server-only"; 4 5export async function getCourse(slug: string) { 6 const response = await fetch( 7 `${process.env.API_URL}/api/courses/${slug}` 8 ); 9 10 if (!response.ok) { 11 return null; 12 } 13 14 return response.json(); 15}
Step 3 — Fetch the Course
1// app/courses/[slug]/page.tsx 2 3import { notFound } from "next/navigation"; 4import { getCourse } from "@/lib/courses"; 5import ProgressButton from "@/components/ProgressButton"; 6 7export default async function CoursePage({ 8 params, 9}: { 10 params: Promise<{ slug: string }>; 11}) { 12 const { slug } = await params; 13 14 const course = await getCourse(slug); 15 16 if (!course) { 17 notFound(); 18 } 19 20 return ( 21 <main> 22 <h1>{course.title}</h1> 23 24 <p>{course.description}</p> 25 26 <ProgressButton courseId={course.id} /> 27 </main> 28 ); 29}
Step 4 — Add the Interactive Component
1// components/ProgressButton.tsx 2 3"use client"; 4 5import { useState } from "react"; 6 7export default function ProgressButton({ 8 courseId, 9}: { 10 courseId: string; 11}) { 12 const [completed, setCompleted] = useState(false); 13 14 function handleComplete() { 15 setCompleted(true); 16 } 17 18 return ( 19 <button onClick={handleComplete}> 20 {completed ? "Course Completed" : "Mark Course Complete"} 21 </button> 22 ); 23}
Now the architecture is:
1CoursePage 2 │ 3 ├── Fetch course data 4 │ 5 ├── Render course content 6 │ 7 └── ProgressButton 8 │ 9 └── Client Component
This is a practical example of combining Server and Client Components rather than choosing only one.
Production Architecture
A larger Next.js application can separate responsibilities like this:
1src/ 2├── app/ 3│ ├── courses/ 4│ │ └── [slug]/ 5│ │ └── page.tsx 6│ │ 7│ └── dashboard/ 8│ └── page.tsx 9│ 10├── components/ 11│ ├── courses/ 12│ ├── forms/ 13│ └── ui/ 14│ 15├── lib/ 16│ ├── auth/ 17│ ├── db/ 18│ └── courses/ 19│ 20├── services/ 21│ ├── courses.ts 22│ └── users.ts 23│ 24└── types/ 25 ├── course.ts 26 └── user.ts
The key principle is separation of responsibilities:
1Route 2 ↓ 3Server Component 4 ↓ 5Service / Data Layer 6 ↓ 7Database / API
while interactive UI follows:
1Server Component 2 ↓ 3Client Component 4 ↓ 5Browser Interaction
Server Components: The Most Important Mental Model
Do not think about Server Components only as a performance feature.
Think about them as an application architecture boundary.
The boundary determines where code executes:
1 Server Boundary 2──────────────────────────────────────── 3 4 SERVER BROWSER 5 6 Database UI Events 7 Secrets useState 8 APIs useEffect 9 Server logic Browser APIs 10 Data fetching Interactive UI 11 12────────────────────────────────────────
Once you understand this boundary, many Next.js decisions become much easier.
Key Takeaways
Server Components are fundamental to the Next.js App Router.
Remember these principles:
- Server Components are the default in the App Router.
- Use Server Components for server-side rendering and data access.
- Server Components can fetch data without automatically requiring
useEffect. - Server-side code can access databases and private services.
- Keep secrets away from Client Components.
- Use Client Components when browser interaction is required.
- Do not make an entire page a Client Component unnecessarily.
- Isolate interactive UI into smaller Client Components.
- Use server-side authentication and authorization for protected operations.
- Think of Server/Client Components as an architectural boundary.
- Use
server-onlyfor modules that must never be imported into client code. - Build applications using a combination of Server and Client Components.
The most useful rule to remember is:
Keep data, secrets, and server logic on the server; move only the interactive parts that need the browser to the client.
Once this principle becomes natural, you can start designing much more scalable Next.js applications.