Skip to main content

Flask: Serving Single-Page Applications

Flask: Serving Single-Page Applications

Single-Page Applications (SPAs) provide dynamic, client-side experiences by loading a single HTML page and updating content via JavaScript. Built on Flask’s lightweight core and leveraging Jinja2 Templating and Werkzeug WSGI, Flask is an ideal backend for serving SPAs, handling API requests, and delivering static assets. This tutorial explores Flask serving Single-Page Applications, covering setup, routing, and practical applications for integrating SPAs with Flask.


01. Why Serve SPAs with Flask?

Flask’s simplicity and flexibility make it perfect for serving SPAs, which rely on frameworks like React, Vue.js, or Angular for frontend rendering. Flask can serve static SPA assets, provide RESTful APIs, and manage backend logic, while Jinja2 Templating and Werkzeug WSGI ensure efficient request handling. This separation of concerns enables scalable, maintainable web applications with rich client-side interactivity.

Example: Basic Flask Setup for SPA

from flask import Flask, send_from_directory

app = Flask(__name__, static_folder='frontend/build', static_url_path='/')

@app.route('/')
def serve_spa():
    return send_from_directory(app.static_folder, 'index.html')

if __name__ == '__main__':
    app.run(debug=True)

Output (visiting /):

Serves the SPA's index.html from frontend/build

Explanation:

  • static_folder - Points to the SPA’s compiled assets.
  • send_from_directory - Serves the SPA’s entry point (index.html).

02. Key Techniques for Serving SPAs

Serving SPAs with Flask involves delivering static assets, handling client-side routing, and providing API endpoints. The table below summarizes key techniques and their applications:

Technique Description Use Case
Static File Serving send_from_directory Deliver SPA assets (HTML, JS, CSS)
Client-Side Routing Catch-all route for SPA Support deep linking and navigation
RESTful API @app.route, jsonify Provide data to the SPA
CORS Handling Flask-CORS Enable API access during development
Production Deployment Configure WSGI server (e.g., Gunicorn) Serve SPA in production


2.1 Serving Static SPA Assets

Example: Serving a React SPA

from flask import Flask, send_from_directory

app = Flask(__name__, static_folder='frontend/build', static_url_path='/')

@app.route('/')
def serve():
    return send_from_directory(app.static_folder, 'index.html')

@app.route('/<path:path>')
def serve_static(path):
    return send_from_directory(app.static_folder, path)

if __name__ == '__main__':
    app.run(debug=True)

React Build Command:

cd frontend
npm run build
mv build ../frontend/build

Explanation:

  • static_folder - Points to the React build directory.
  • /<path:path> - Serves static assets like JS and CSS.

2.2 Handling Client-Side Routing

Example: Supporting SPA Routing

from flask import Flask, send_from_directory
import os

app = Flask(__name__, static_folder='frontend/build', static_url_path='/')

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
    if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
        return send_from_directory(app.static_folder, path)
    return send_from_directory(app.static_folder, 'index.html')

if __name__ == '__main__':
    app.run(debug=True)

Output (visiting /dashboard):

Serves index.html for client-side routing

Explanation:

  • Catch-all route returns index.html for SPA routes.
  • Checks for existing static files before serving index.html.

2.3 Providing a RESTful API

Example: Flask API for SPA

from flask import Flask, jsonify, send_from_directory
import os

app = Flask(__name__, static_folder='frontend/build', static_url_path='/')

@app.route('/api/data')
def get_data():
    return jsonify({'message': 'Data from Flask', 'items': [1, 2, 3]})

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
    if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
        return send_from_directory(app.static_folder, path)
    return send_from_directory(app.static_folder, 'index.html')

if __name__ == '__main__':
    app.run(debug=True)

Output (fetching /api/data):

{
  "message": "Data from Flask",
  "items": [1, 2, 3]
}

Explanation:

  • /api/data - Provides JSON data for the SPA.
  • Separates API routes from static file serving.

2.4 Enabling CORS for Development

Example: Using Flask-CORS

from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

@app.route('/api/data')
def get_data():
    return jsonify({'message': 'CORS-enabled data'})

if __name__ == '__main__':
    app.run(debug=True)

React Fetch (App.js):

useEffect(() => {
  fetch('http://localhost:5000/api/data')
    .then(res => res.json())
    .then(data => console.log(data));
}, []);

Explanation:

  • Flask-CORS - Allows the SPA (e.g., on localhost:3000) to access the Flask API.
  • Essential during development when frontend and backend run on different ports.

2.5 Production Deployment

Example: Gunicorn Setup

from flask import Flask, send_from_directory
import os

app = Flask(__name__, static_folder='frontend/build', static_url_path='/')

@app.route('/api/data')
def get_data():
    return {'message': 'Production data'}

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
    if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
        return send_from_directory(app.static_folder, path)
    return send_from_directory(app.static_folder, 'index.html')

if __name__ == '__main__':
    app.run()

Deployment Commands:

pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 app:app

Explanation:

  • gunicorn - Serves the Flask app in production.
  • Static assets and API are served from the same server.

2.6 Incorrect Setup

Example: Missing Client-Side Routing

from flask import Flask, send_from_directory

app = Flask(__name__, static_folder='frontend/build')

@app.route('/')
def serve():
    return send_from_directory(app.static_folder, 'index.html')

if __name__ == '__main__':
    app.run(debug=True)

Output (visiting /dashboard):

404 Not Found

Explanation:

  • Missing catch-all route breaks SPA routing.
  • Solution: Implement /<path:path> to serve index.html.

03. Effective Usage

3.1 Recommended Practices

  • Use a catch-all route to support client-side routing.

Example: Comprehensive SPA Serving

from flask import Flask, jsonify, send_from_directory
from flask_cors import CORS
import os

app = Flask(__name__, static_folder='frontend/build', static_url_path='/')
CORS(app)

@app.route('/api/users')
def get_users():
    return jsonify([
        {'id': 1, 'name': 'Alice'},
        {'id': 2, 'name': 'Bob'}
    ])

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
    if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
        return send_from_directory(app.static_folder, path)
    return send_from_directory(app.static_folder, 'index.html')

if __name__ == '__main__':
    app.run(debug=True)

React Frontend (App.js):

import React, { useState, useEffect } from 'react';
import { BrowserRouter, Route, Routes, Link } from 'react-router-dom';

function Home() {
  const [users, setUsers] = useState([]);
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => setUsers(data));
  }, []);
  return (
    <div>
      <h1>Users</h1>
      <ul style="padding: 0px 0px 0px 20px; margin-top: 0px;">
        {users.map(user => <li key={user.id}>{user.name}</li>)}
      </ul>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link> | <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<h1>About</h1>} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
  • Serve static assets and APIs from distinct routes.
  • Use CORS during development, restrict origins in production.

3.2 Practices to Avoid

  • Avoid serving unoptimized SPA builds in production.

Example: Serving Development Build

from flask import Flask, send_from_directory

app = Flask(__name__, static_folder='frontend/public')  # Incorrect: Dev folder

@app.route('/')
def serve():
    return send_from_directory(app.static_folder, 'index.html')

if __name__ == '__main__':
    app.run(debug=True)

Output:

Slow, unoptimized SPA performance
  • Development builds are not optimized for production.
  • Solution: Use npm run build and serve the build folder.

04. Common Use Cases

4.1 User Dashboard SPA

Serve a dashboard SPA with Flask API for user data.

Example: User Dashboard

from flask import Flask, jsonify, send_from_directory
from flask_cors import CORS
import os

app = Flask(__name__, static_folder='frontend/build', static_url_path='/')
CORS(app)

@app.route('/api/users')
def get_users():
    return jsonify([
        {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
    ])

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
    if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
        return send_from_directory(app.static_folder, path)
    return send_from_directory(app.static_folder, 'index.html')

if __name__ == '__main__':
    app.run(debug=True)

Vue.js Frontend (App.vue):

<template>
  <div>
    <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link>
    <router-view />
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

<!-- src/views/Home.vue -->
<template>
  <div>
    <h1>User Dashboard</h1>
    <ul style="padding: 0px 0px 0px 20px; margin-top: 0px;">
      <li v-for="user in users" :key="user.id">{{ user.name }} - {{ user.email }}</li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return { users: [] };
  },
  async created() {
    const response = await axios.get('/api/users');
    this.users = response.data;
  }
};
</script>

Explanation:

  • Flask serves the SPA and user data API.
  • Vue.js handles client-side routing and data display.

4.2 Blog SPA

Serve a blog SPA with Flask API for posts.

Example: Blog Application

from flask import Flask, jsonify, send_from_directory
from flask_cors import CORS
import os

app = Flask(__name__, static_folder='frontend/build', static_url_path='/')
CORS(app)

@app.route('/api/posts')
def get_posts():
    return jsonify([
        {'id': 1, 'title': 'First Post', 'content': 'Hello, world!'}
    ])

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
    if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
        return send_from_directory(app.static_folder, path)
    return send_from_directory(app.static_folder, 'index.html')

if __name__ == '__main__':
    app.run(debug=True)

React Frontend (App.js):

import React, { useState, useEffect } from 'react';
import { BrowserRouter, Route, Routes, Link } from 'react-router-dom';

function Blog() {
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(data => setPosts(data));
  }, []);
  return (
    <div>
      <h1>Blog</h1>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Blog</Link> | <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Blog />} />
        <Route path="/about" element={<h1>About</h1>} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Explanation:

  • Flask provides blog post data via API.
  • React handles client-side rendering and routing.

Conclusion

Serving SPAs with Flask, powered by Jinja2 Templating and Werkzeug WSGI, enables developers to build modern, dynamic web applications. Key takeaways:

  • Use send_from_directory to serve SPA assets.
  • Implement catch-all routes for client-side routing.
  • Provide RESTful APIs for data and use CORS during development.
  • Avoid serving unoptimized builds or missing routing support.

With Flask, you can efficiently serve SPAs, combining the power of client-side frameworks with a robust backend for seamless, scalable applications!

Comments