Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    A pattern for composable UI in Flask

    The component model of web development has won. But how do you bring that innovation back to traditional frameworks? I've been trying to figure that out.

    Flask?

    Flask is a typical 2010's web framework like Rails, Django, Express and others. You have views, models, templates, controllers and all the rest.

    Requests come in, get routed to a view, HTML "renders" on the server, flies back as a string. Sprinkle JavaScript to make smol things interactive.

    This works great until your team starts to grow. It encourages blobby monolithic architectures with unclear separation of concerns. Big ball of mud style.

    Why make things composable

    Balls of mud are great for small applications. Every engineer knows how everything works, has the codebase memorized, and can move fast.

    Then you add engineers and features. Code begins to break every time you sneeze.

    The feature you built 3 months ago has changed 30 times since you looked. Others did that. You don't even know how it works anymore.

    When I first encountered this a few years ago, it felt like gerbils running around my brain moving things around. Little gremlins messing up my beautiful mental model of the code.

    The solution is to compose your code from small balls of mud. Keep features self-contained and independent so they're easy to move around.

    Composable Flask (and others?)

    I'm not inventing anything new here. Just a different way to hold existing tools. A different way to think about it.

    How these apps tend to look

    A typical Flask view looks something like this:

    # views.py
    
    @views.route("/cool_feature")
    def cool_feature():
    	# bunch of data fetching
    	# bunch of logic
    	# instantiating forms
    
    	return render_template("cool_feature.html",
    			user=user,
    			user_form=user_form,
    			address_form=address_form,
    			hello_string=hello_string,
    			# ...
    	)
    

    And then you have a template that renders this:

    <!-- html stuff -->
    
    {% if user %}
    	<p>{{ hello_string }}, {{ user.name }}!!!</p>
    	<p>So cool to see you! Look we say hello in different languages so you know we're quirky and fun</p>
    {% endif %}
    
    {% if not user.name %}
    <h3>Please give us your name</h3>
    <form action={{ url_for('views.update_name') }}>
    	{{ form.hidden_tag() }}
    	<input name="name" type="text" />
    	<button type="submit">Save name</button>
    </form>
    {% endif %}
    
    <!-- ... -->
    

    And so on like this. You end up with huge views and massive templates full of intertwined logic. Everything declared and loaded up-front.

    Want to move your form somewhere else? Tough! You gotta move allll the stuff it comes with. Hope you didn't miss any.

    Plus if any of those sub features on the page break, the whole page breaks. You get an error like Oh no! Line 1723 of blah.py and groan. Hate those 🙃

    Make it composable

    You can rewrite that same thing as composable views. Even better if you organize them into blueprints or folders by business domain.

    Small views:

    # user/views.py
    
    @user.route("/user/hello")
    def hello():
    	# get user
    	return render_template("user/hello.html",
    		user=user,
    		hello_string=hello_string
    	)
    
    @user.route("/user/name_form")
    def name_form():
    	# init forms
    	return render_template("user/name_forms.html,
    		user_form=user_form
      )
    
    # views.py
    import views from .user as user_views
    
    @views.route("/cool_feature")
    def cool_feature():
    	# almost nothing
    
    	return render_template("cool_feature.html",
    		user=user
    		user_views=user_views,
    		# ...
    	)
    

    With small templates:

    <!-- user/templates/hello.html -->
    
    <p>{{ hello_string }}, {{ user.name }}!!!</p>
    <p>So cool to see you! Look we say hello in different languages so you know we're quirky and fun</p>
    
    <!-- user/templates/name_form.html -->
    
    <h3>Please give us your name</h3>
    <form action={{ url_for('views.update_name') }}>
    	{{ form.hidden_tag() }}
    	<input name="name" type="text" />
    	<button type="submit">Save name</button>
    </form>
    
    <!-- cool_feature.html -->
    
    {% if user %}
    	{{ user_views.hello() | safe }}
    {% endif %}
    
    {% if not user.name %}
    	{{ user_views.name_form() | safe}}
    {% endif %}
    
    <!-- ... -->
    

    What just happened

    Those function calls are almost like React components. Function calls that return markup.

    Each handles its own data access, styling, javascript, and all the rest. The view manages data, template manages the rest. You can use {% block %} instructions to bring your own styling or javascript and let Flask put it all together in the base template.

    That means you can move your code around without worrying about breaking anything or missing a dependency.

    Now here's the best part: Every composable fragment of your UI comes with a URL. That means with a bit of JavaScript, you can update portions of your page without a page reload.

    Submit the form, re-render just the form. And that pretty much gives you HTMX. Or any of the newfangled React meta frameworks.

    But why?

    I realized going full React Islands will be a jump too far. Too much rewrite.

    We can use islands for new features and big bets. But we also need to keep working on our existing 65,000 lines of python and 46,000 lines of HTML. Many of those will never get the React treatment – not worth it.

    Pretty happy with this composable UI pattern. ✌️

    Cheers,
    ~Swizec

    Published on February 8th, 2025 in Technical, Fullstack Web, Flask, Architectural Complexity

    Did you enjoy this article?

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev

    Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️

    Created by Swizec with ❤️