The Fundamental Question: Where Does Code Run?
One of the most important concepts in web development is understanding where your code executes. This isn't just a technical detail—it has profound implications for security, performance, user experience, and how you architect your entire application.
When you write a web application, you're actually writing code that runs in two completely different environments:
- Server-side code runs on machines you control (your servers, cloud instances)
- Client-side code runs on machines you DON'T control (users' browsers, phones)
This distinction affects everything from what languages you can use, to what data you can access, to how secure your application can be.
A Helpful Analogy: Movie Production
Think of it like making and showing a movie:
- Server-side (The Movie Studio): This is where the magic happens behind closed doors. The studio has expensive equipment, professional staff, proprietary techniques, and tight security. Viewers never see the raw footage, special effects breakdown, or production secrets. You control everything here.
- Client-side (The Movie Theater): This is where the final product is experienced. The theater (browser) just receives and displays what you send it. The viewer can pause, rewind, adjust brightness—but they can't change the movie itself. However, nothing stops them from recording the screen or analyzing what they see.
In web development, code executes in two distinct environments:
- Server-side (Backend): Code runs on YOUR servers. You control the environment completely. Languages: Java, Python, Node.js, Go, C#, Ruby, etc.
- Client-side (Frontend): Code runs in the USER'S browser. You send code to them, but they control the environment. Languages: JavaScript (primarily), plus HTML and CSS for structure and styling.
Why This Matters
Understanding this distinction is crucial because:
- Security: Code on your server is private; code sent to the browser is public. Never put secrets in client-side code!
- Trust: You can trust server-side operations; you can NEVER trust client-side data or validation.
- Capabilities: Servers can access databases, file systems, and external services directly. Browsers are sandboxed for security.
- Performance: Server processing happens once; client processing happens on each user's device.
- User Experience: Client-side code provides instant feedback; server-side operations require network round-trips.
Visual Overview: The Complete Picture
// Visual overview:
┌─────────────────────────────────────────────────────────────────┐
│ YOUR SERVER │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Java / Spring Boot │ │
│ │ - Business logic │ │
│ │ - Database access │ │
│ │ - Authentication │ │
│ │ - API endpoints │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
HTTP Response
(HTML, JSON, etc.)
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ USER'S BROWSER │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ JavaScript / HTML / CSS │ │
│ │ - UI rendering │ │
│ │ - User interactions │ │
│ │ - Form validation │ │
│ │ - Animations │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Server-Side Execution (Backend)
// Server-side code runs on YOUR infrastructure
// Language: Java, Python, C#, Node.js, Go, etc.
@RestController
public class UserController {
@Autowired
private UserRepository userRepository; // Database access
@Autowired
private PasswordEncoder encoder; // Security
@PostMapping("/api/register")
public User register(@RequestBody RegisterRequest request) {
// ALL of this runs on the SERVER:
// 1. Validate data (server-side validation is REQUIRED)
if (userRepository.existsByEmail(request.getEmail())) {
throw new EmailExistsException();
}
// 2. Hash password (NEVER trust client with security)
String hashedPassword = encoder.encode(request.getPassword());
// 3. Save to database (client can't access DB directly)
User user = new User();
user.setEmail(request.getEmail());
user.setPassword(hashedPassword);
return userRepository.save(user);
}
}
What Runs on the Server
| Task | Why Server? |
|---|---|
| Database operations | Security - clients can't access DB directly |
| Authentication | Security - passwords, tokens, sessions |
| Business logic | Consistency - same rules for all clients |
| Data validation | Security - client validation can be bypassed |
| File processing | Performance - heavy computation |
| Third-party API calls | Security - hide API keys |
| Email sending | Security - SMTP credentials |
Client-Side Execution (Frontend)
// Client-side code runs in the USER'S BROWSER
// Language: JavaScript (or TypeScript compiled to JS)
// React/JavaScript example
function RegistrationForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
// ALL of this runs in the BROWSER:
// 1. Handle user input
const handleEmailChange = (e) => {
setEmail(e.target.value);
};
// 2. Client-side validation (for UX, not security!)
const validateForm = () => {
if (!email.includes('@')) {
setError('Invalid email');
return false;
}
return true;
};
// 3. Submit to server
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) return;
// Send data to server for REAL processing
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
};
// 4. Render UI
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={handleEmailChange} />
<button type="submit">Register</button>
</form>
);
}
What Runs on the Client
| Task | Why Client? |
|---|---|
| UI rendering | User sees it - must be in browser |
| User interactions | Immediate response - no round trip |
| Form validation (UX) | Instant feedback - better experience |
| Animations | Smooth - no network latency |
| Local storage | Offline capability, preferences |
| Single Page App routing | Fast navigation without reload |
The Complete Picture
// Example: User submits a login form
// ═══════════════════════════════════════════════════════════════
// STEP 1: CLIENT - User fills form (Browser/JavaScript)
// ═══════════════════════════════════════════════════════════════
// JavaScript in browser
const form = document.getElementById('loginForm');
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Client-side validation (instant feedback)
const email = document.getElementById('email').value;
if (!email) {
showError('Email required'); // Immediate, no server needed
return;
}
// Send to server
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
});
// ═══════════════════════════════════════════════════════════════
// STEP 2: SERVER - Process login (Your Server/Java)
// ═══════════════════════════════════════════════════════════════
@PostMapping("/api/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// Server-side validation (REQUIRED - client can be bypassed)
if (request.getEmail() == null) {
return ResponseEntity.badRequest().body("Email required");
}
// Database lookup (only server can do this)
User user = userRepository.findByEmail(request.getEmail());
if (user == null) {
return ResponseEntity.status(401).body("Invalid credentials");
}
// Password verification (server has the hash)
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
return ResponseEntity.status(401).body("Invalid credentials");
}
// Generate JWT token (server secret key)
String token = jwtService.generateToken(user);
return ResponseEntity.ok(new LoginResponse(token));
}
// ═══════════════════════════════════════════════════════════════
// STEP 3: CLIENT - Handle response (Browser/JavaScript)
// ═══════════════════════════════════════════════════════════════
// Back in the browser
if (response.ok) {
const { token } = await response.json();
localStorage.setItem('token', token); // Store locally
window.location.href = '/dashboard'; // Navigate
} else {
showError('Login failed'); // Update UI
}
Security: Never Trust the Client
NEVER trust data from the client. Everything can be manipulated!
// ❌ BAD: Trusting client-side validation only
// Client (JavaScript) - can be bypassed!
if (age >= 18) {
submitOrder(); // User can remove this check in browser DevTools
}
// Server (Java) - no validation
@PostMapping("/order")
public void createOrder(@RequestBody Order order) {
orderRepository.save(order); // 💀 Saves anything!
}
// ═══════════════════════════════════════════════════════════════
// ✅ GOOD: Server-side validation (always!)
// Client (JavaScript) - for UX only
if (age >= 18) {
submitOrder(); // Nice UX, but not security
}
// Server (Java) - THE REAL CHECK
@PostMapping("/order")
public void createOrder(@RequestBody Order order) {
// Validate EVERYTHING on server
if (order.getUser().getAge() < 18) {
throw new ValidationException("Must be 18+");
}
if (order.getQuantity() <= 0) {
throw new ValidationException("Invalid quantity");
}
orderRepository.save(order);
}
What Can Be Manipulated on Client
// EVERYTHING the client sends can be forged:
// 1. Form data
// User can change hidden fields, prices, quantities
// 2. JavaScript validation
// Can be disabled in browser DevTools
// 3. HTTP headers
// Can be modified with curl or Postman
// 4. Cookies (if not HttpOnly)
// Can be read/modified via JavaScript
// 5. Local storage
// User has full access
// Example attack:
curl -X POST https://shop.com/api/order \
-H "Content-Type: application/json" \
-d '{"productId": 1, "price": 0.01}' # Changed price!
// Server MUST verify price from database, not trust the request
Server-Side Rendering vs Client-Side Rendering
Server-Side Rendering (SSR)
// Server generates complete HTML
// Thymeleaf (Java template engine)
@GetMapping("/products")
public String productList(Model model) {
List<Product> products = productService.findAll();
model.addAttribute("products", products);
return "products"; // Renders products.html template
}
<!-- products.html -->
<html>
<body>
<h1>Products</h1>
<ul>
<!-- Server fills this in BEFORE sending to browser -->
<li th:each="product : ${products}">
<span th:text="${product.name}">Product Name</span>
</li>
</ul>
</body>
</html>
// Browser receives COMPLETE HTML - no JavaScript needed
Client-Side Rendering (CSR)
// Server sends minimal HTML + JavaScript
// JavaScript fetches data and builds UI
// Server (Java) - just an API
@GetMapping("/api/products")
public List<Product> getProducts() {
return productService.findAll(); // Returns JSON
}
// Client (React/JavaScript) - builds the UI
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => setProducts(data));
}, []);
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
| Aspect | Server-Side Rendering | Client-Side Rendering |
|---|---|---|
| Initial load | Faster (HTML ready) | Slower (wait for JS) |
| SEO | Better (content visible) | Needs extra work |
| Interactivity | Page reloads | Smooth SPA experience |
| Server load | Higher (renders HTML) | Lower (just API) |
| Examples | Thymeleaf, JSP | React, Vue, Angular |
Modern Hybrid Approaches
// Modern apps often combine both approaches
// 1. Next.js (React) - SSR + CSR
// - Server renders initial page (fast, SEO)
// - Client takes over for interactivity
// 2. Spring Boot + Thymeleaf + HTMX
// - Server renders HTML fragments
// - HTMX swaps them without full page reload
@GetMapping("/products/search")
public String searchProducts(
@RequestParam String query,
Model model) {
model.addAttribute("products", productService.search(query));
return "fragments/product-list"; // Just the list HTML
}
<!-- HTML with HTMX -->
<input type="search"
hx-get="/products/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
<div id="results">
<!-- Server-rendered HTML inserted here -->
</div>
Quick Reference
Server-Side (Java)
- Database operations
- Authentication & authorization
- Business logic & rules
- Data validation (REQUIRED)
- API keys & secrets
- Heavy computations
- Email/SMS sending
Client-Side (JavaScript)
- UI rendering & updates
- User interactions
- Form validation (for UX)
- Animations & transitions
- Local storage access
- SPA navigation
- Real-time updates (with WebSocket)
Summary
- Server-side: Runs on your servers - Java, secure, database access
- Client-side: Runs in browser - JavaScript, UI, interactions
- Security: NEVER trust client data - always validate on server
- SSR: Server generates HTML - fast initial load, good SEO
- CSR: JavaScript builds UI - smooth SPA experience
- Modern apps: Often combine both approaches