Serving Web Pages the right Way: SSR vs CSR

When I first learned Flask, I was serving Jinja templates/html pages directly from my Backend. Later I learned to keep Frontend and Backend seperate, and serve APIs and use them from my frontend to fetch data and show it on the web page with JavaScript. The previous way felt wrong, I started thinking that this is the right way and that was wrong way.
There is no only right way when it comes to tech. Whatever maximizes performance and minimizes cost is the best way to go.
There are four rendering strategies I know about, we will discuss two here and leave the rest 2 for later parts.
Client Side Rendering (CSR):
You might have created weather apps that utilize weather Apis. If not then also you might have used openiAI/Youtube/Spotify Apis? Well, they definitely have more complex structure, but similar to that, if your server sends API responses and your javascript frontend uses the json data and renders everything in browser then that is your client side rendering. Because the process of rendering a page happens on the client side after the data has come to the browser.
Let us see code for it-
from flask import Flask, jsonify
app = Flask(__name__)
data = {"usersData": [{"id": "08696", "username": "Priya", "age": "24", "company": "Paytm", "department": "Marketing"}, {"id": "55296", "username": "Pratap", "age": "24", "company": "Myntra", "department": "IT"}, {"id": "86506", "username": "Ruchi", "age": "24", "company": "Nike", "department": "Finance"}, {"id": "056996", "username": "Veer", "age": "24", "company": "Toyota", "department": "Human Resource"}]}
@app.route("/users", methods=['GET'])
def allUsers():
return jsonify(data)
Here, the data is sent to client in json format and now is upto him, how he wants to handle it. This is how it looks on my FireFox browser when I try to open this url-

Let us create a simple html and we’ll also have to tweak this python server a bit, because there is this CORS policy that won’t let cross origin server interact easily.
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": ["http://127.0.0.1:5500", "http://localhost:5500"]}})
data = {"usersData": [{"id": "08696", "username": "Priya", "age": "24", "company": "Paytm", "department": "Marketing"}, {"id": "55296", "username": "Pratap", "age": "24", "company": "Myntra", "department": "IT"}, {"id": "86506", "username": "Ruchi", "age": "24", "company": "Nike", "department": "Finance"}, {"id": "056996", "username": "Veer", "age": "24", "company": "Toyota", "department": "Human Resource"}]}
@app.route("/users", methods=['GET'])
def allUsers():
return jsonify(data)
if __name__ == "__main__":
app.run(debug=True)
and my html file goes like-
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Users Data</title>
</head>
<body>
<h1>Users data</h1>
<ol id="usersList"></ol>
<script>
// Fetch data from Flask backend
fetch("http://127.0.0.1:5000/users", {
method: 'GET',
mode: 'cors'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
const list = document.getElementById("usersList");
data.usersData.forEach(user => {
const li = document.createElement("li");
li.textContent = `${user.username} (${user.company} - ${user.department})`;
list.appendChild(li);
});
})
.catch(error => {
console.error('Fetch error:', error);
});
</script>
</body>
</html>
The fetch method, here, calls my backend to ‘GET‘ the response in Json format, which then I add into ‘list’ and for each user in user data we append a ‘li‘ tag. And the result is-

Server Side Rendering (SSR):
Server side rendering is a bit more simpler than that according to me. When the backend serves html directly, without needing to setup a separate frontend that would talk to it through API calls. Then that is Server Side Rendering. It is called so because the html is already build, data is already there on it because it even goes to browser, now as it reaches client it gets painted on the browser for user to see.
Code for it-
from flask import Flask, render_template
from flask_cors import CORS
app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": ["http://127.0.0.1:5500", "http://localhost:5500"]}})
data = {"usersData": [{"id": "08696", "username": "Priya", "age": "24", "company": "Paytm", "department": "Marketing"}, {"id": "55296", "username": "Pratap", "age": "24", "company": "Myntra", "department": "IT"}, {"id": "86506", "username": "Ruchi", "age": "24", "company": "Nike", "department": "Finance"}, {"id": "056996", "username": "Veer", "age": "24", "company": "Toyota", "department": "Human Resource"}]}
@app.route("/users", methods=['GET'])
def allUsers():
return render_template('index.html', users=data["usersData"])
if __name__ == "__main__":
app.run(debug=True)
Our Jinja-Html code-
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Users Data</title>
</head>
<body>
<h1>Users data</h1>
<ol id="usersList">
{% for user in users %}
<li>{{ user.username }} - ({{ user.company }} - {{user.department}})</li>
{% endfor %}
</ol>
</body>
</html>
And here is our result-

Here is out file structure for both-

Although the output looks pretty much the same, these are two different styles of writing code, rendering web pages and also makes a difference in terms of SEO (search engine optimization), interactivity and speed!
Summary
CSR
What happens:
The browser downloads a mostly-empty HTML file.
JavaScript fetches data from APIs.
The browser builds the UI dynamically.
Pros:
Excellent for single-page apps (SPAs).
Faster page transitions once loaded.
Easy separation of frontend (React/Vue/JS) and backend (Flask, Django REST, etc.).
Cons:
Slower first load (user would see a blank screen until JS fetches data. - rarely happens but - the concept)
SEO is trickier because search engines prefer pre-rendered HTML.
Requires more browser resources.
SSR
What happens:
The server prepares and sends a complete HTML page (with data already inside).
The browser just paints it (no fetch or JS required to view it.)
Pros:
Faster first contentful paint (page appears immediately).
Better for SEO (search bots see complete HTML).
Simpler to build and host (just Flask + templates).
Cons:
Every interaction usually needs a new request/response cycle.
Harder to make dynamic, real-time UI updates.
Mixing logic and HTML can grow messy for large apps.
There are two more strategies left:
Incremental Static Regeneration
Static Site Generation
I haven’t tried them yet. But I’ll try them and we might discuss them in next Part.
Thanks for reading. Seeya!
resources:
https://www.educative.io/answers/ssr-vs-csr-vs-isr-vs-ssg
https://www.geeksforgeeks.org/websites-apps/cross-origin-resource-sharing-cors/
https://flask.palletsprojects.com/en/stable/
