Flask is a Python web framework that works for both small and large web applications.
In this guide, we'll walk through developing a Flask application that uses Lakebase Postgres. We'll cover setting up the project structure, defining models, creating routes, and handling database migrations. We'll also build a frontend with Tailwind CSS.
Prerequisites
- Python 3.7 or later installed
- Node.js 18 or later
- A Neon account for Postgres hosting
- Basic familiarity with Flask and SQLAlchemy
Project setup
-
Create a new directory and set up a virtual environment:
mkdir flask-neon-app cd flask-neon-app python -m venv venv source venv/bin/activate # On Windows, use `venv\Scripts\activate` -
Install required packages:
pip install flask flask-sqlalchemy psycopg2-binary python-dotenv flask-migrateCreate a
requirements.txtfile for package management:pip freeze > requirements.txtThis file pins your dependencies so development and deployment use the same versions.
-
Create a
.envfile for environment variables:DATABASE_URL=postgresql://user:password@your-neon-host:5432/your-databaseReplace
user,password,your-neon-host, andyour-databasewith the values from your Neon connection string. Click Connect in the Neon Console nav to find them.
Application structure
For small applications, you can keep all code in a single file. As your application grows, organize your code into separate modules, with models, routes, services, and utilities in different directories.
A typical Flask application structure might look like this:
flask-neon-app/
├── app/
│ ├── __init__.py
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── static/
│ │ └── css/
│ ├── templates/
│ └── utils/
├── config.py
├── requirements.txt
├── run.py
└── tailwind.config.jsThis structure separates concerns and makes it easier to maintain and scale your application. The app directory contains the core application logic, while other files and directories handle configuration, dependencies, and frontend assets.
Database configuration
You can think of the app/__init__.py file as the entry point for your Flask application. It initializes the Flask app, sets up the database connection, and registers blueprints for routing.
By adding the __init__.py file, you transform the app directory into a Python package. This allows you to import modules from the app package in other files.
The app directory name is a common convention for Flask applications, but you can choose a different name if you prefer based on your project needs.
With that in mind, let's set up the database connection and initialize the Flask app. In app/__init__.py, add the following code:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from dotenv import load_dotenv
import os
load_dotenv()
db = SQLAlchemy()
migrate = Migrate()
def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
migrate.init_app(app, db)
# Register blueprints here
from app.routes import user_routes
app.register_blueprint(user_routes.user_bp)
return appThis setup initializes Flask, SQLAlchemy, and Flask-Migrate. It loads the database URL from the environment variables and disables SQLAlchemy's modification tracking for better performance.
Blueprints are a way to organize related routes and views in Flask applications. We will cover blueprints and routes in the next sections.
To learn more about Flask-Migrate, check out the Managing database migrations and schema changes with Flask and Lakebase Postgres guide.
Model definition
In web applications, models represent the data structure and relationships in your database. In the context of Flask and SQLAlchemy, models are Python classes that map to database tables and also allow you to define the schema and relationships between tables.
As an example of a typical model definition, let's create a User model in app/models/user.py:
from app import db
from datetime import datetime
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f'<User {self.username}>'
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'created_at': self.created_at.isoformat()
}This model defines a User with username, email, and created_at fields. The __repr__ method provides a string representation of the model instance for debugging purposes and logging. The to_dict method allows easy serialization of the model to JSON which is useful when returning data from API endpoints.
Route creation with blueprints
In Flask, blueprints organize your application into components. They group related routes, view functions, templates, and static files, so you can separate the functional areas of a large project.
If you're familiar with Laravel, blueprints in Flask serve a similar purpose to Laravel's controllers and route groups, allowing you to logically organize your routes and associated functionality.
Blueprints also help you avoid naming conflicts between different parts of your application.
Here's an expanded example of how to use blueprints in a Flask application, in a file named app/routes/user_routes.py for user-related routes:
from flask import Blueprint, jsonify, request, render_template, redirect, url_for
from app.models.user import User
from app import db
# Create a blueprint named 'user' with a URL prefix '/user'
user_bp = Blueprint('user', __name__, url_prefix='/user')
# Route for creating a new user (HTML form submission)
@user_bp.route('/create', methods=['POST'])
def create_user():
data = request.form
new_user = User(username=data['username'], email=data['email'])
db.session.add(new_user)
db.session.commit()
return redirect(url_for('user.list_users'))
# Route for displaying all users (HTML)
@user_bp.route('/list', methods=['GET'])
def list_users():
users = User.query.all()
return render_template('users.html', users=users)
# API route for retrieving all users (JSON)
@user_bp.route('/api/list', methods=['GET'])
def get_users_api():
users = User.query.all()
return jsonify([user.to_dict() for user in users])
# Route for displaying a single user (HTML)
@user_bp.route('/<int:user_id>', methods=['GET'])
def get_user(user_id):
user = User.query.get_or_404(user_id)
return render_template('user_detail.html', user=user)
# API route for retrieving a single user (JSON)
@user_bp.route('/api/<int:user_id>', methods=['GET'])
def get_user_api(user_id):
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())Let's break down this code:
-
We import necessary modules and create a blueprint named 'user' with a URL prefix '/user'. This prefix will be prepended to all routes defined within this blueprint which helps in organizing routes into logical groups. If you have multiple blueprints, each can have its own URL prefix.
-
create_user(): This route handles POST requests to create a new user. It reads form data, creates a newUserinstance, adds it to the database session, and commits the transaction. It then redirects to thelist_users()route. -
list_users(): This route displays all users in HTML format. It queries all users from the database and renders a template with the user data. -
get_users_api(): This API route returns all users in JSON format. It queries all users from the database, converts them to dictionaries using theto_dict()method, and returns a JSON response. -
get_user(): This route displays details of a single user in HTML format. It retrieves a user by ID or returns a 404 error if the user doesn't exist. -
get_user_api(): This API route returns details of a single user in JSON format. It retrieves a user by ID or returns a 404 error if the user doesn't exist.
To use this blueprint in your main Flask application, you would register it like this in app/__init__.py which we've seen earlier:
from flask import Flask
from app.routes.user_routes import user_bp
app = Flask(__name__)
app.register_blueprint(user_bp)This keeps related routes together as your application grows.
Frontend with Tailwind CSS and templates
Tailwind CSS is a utility-first CSS framework. It provides low-level utility classes that you apply directly in your HTML.
To integrate Tailwind CSS with Flask templates, you can follow these steps:
-
Install Tailwind CSS:
npm init -y npm install tailwindcss npx tailwindcss initThis initializes a new Node.js project, installs Tailwind CSS, and creates a basic Tailwind configuration file.
-
Update the Tailwind CSS configuration file
tailwind.config.jsto include your HTML templates:module.exports = { content: ['./app/templates/**/*.html'], theme: { extend: {}, }, plugins: [], };This configuration tells Tailwind to scan your HTML templates for classes to include in the final CSS output. The
extendkey allows you to customize Tailwind's default theme. -
Create
app/static/css/main.css:@tailwind base; @tailwind components; @tailwind utilities;This file imports Tailwind's base styles, component classes, and utility classes. It serves as the entry point for Tailwind to inject its styles.
-
Compile Tailwind CSS:
npx tailwindcss -i ./app/static/css/main.css -o ./app/static/css/output.css --watchThis command compiles your Tailwind CSS file and watches for changes. The output only includes the styles your templates use, which keeps the CSS file small as your application grows.
The
--watchflag recompiles the CSS whenever you change your HTML files. -
Create a base template in
app/templates/base.html:<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>{% block title %}Flask Neon App{% endblock %}</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}" /> </head> <body class="bg-gray-100"> <nav class="bg-blue-500 p-4 text-white"> <div class="container mx-auto"> <a href="/" class="text-2xl font-bold">Flask Neon App</a> </div> </nav> <main class="container mx-auto mt-8">{% block content %}{% endblock %}</main> </body> </html>This base template sets up the basic structure of your HTML pages. It includes:
- A title block that can be overridden in child templates
- A link to the compiled Tailwind CSS file
- A simple navigation bar with Tailwind classes for styling
- A main content area with a block that child templates can fill thanks to Jinja2 template inheritance
-
Create
app/templates/users.html:{% extends "base.html" %} {% block title %}Users{% endblock %} {% block content %} <h1 class="mb-4 text-3xl font-bold">Users</h1> <div class="rounded bg-white p-4 shadow-md"> <form action="{{ url_for('user.create_user') }}" method="post" class="mb-4"> <input type="text" name="username" placeholder="Username" required class="mr-2 border p-2" /> <input type="email" name="email" placeholder="Email" required class="mr-2 border p-2" /> <button type="submit" class="bg-blue-500 rounded px-4 py-2 text-white">Add User</button> </form> <ul> {% for user in users %} <li class="mb-2">{{ user.username }} ({{ user.email }})</li> {% endfor %} </ul> </div> {% endblock %}This template extends the base template using
{% extends "base.html" %}and provides specific content for the users page. It includes:- A form for adding new users, styled with Tailwind classes
- A list of existing users, also styled with Tailwind
- Jinja2 template syntax for dynamic content (e.g.,
{% for user in users %})
Database migrations
Database migrations let you change your database structure incrementally, keeping it in sync with your application's models. This guide uses Flask-Migrate, a Flask extension that handles SQLAlchemy database migrations using Alembic.
To set up and use Flask-Migrate:
-
Initialize the migration repository:
flask db initThis command creates a new migration repository. It sets up a
migrationsdirectory with the necessary files for managing your migrations. -
Create a migration:
flask db migrate -m "Initial migration"This command creates a new migration script based on the changes detected in your models. Flask-Migrate detects changes to your models and generates the migration script, so you don't have to write it by hand.
The
-mflag allows you to provide a brief description of the migration, which is helpful for tracking changes over time. -
Apply the migration:
flask db upgradeThis command applies the migration to your database, making the necessary schema changes.
Review the generated migration scripts before applying them, especially in production. Flask-Migrate is generally good at detecting changes, but complex modifications might require manual adjustments to the migration scripts.
For more advanced usage, Flask-Migrate provides additional commands:
flask db downgrade: Reverts the last migrationflask db current: Displays the current revision of the databaseflask db history: Shows the migration history
You should commit your migration files to version control so that all developers and deployment environments can maintain consistent database schemas.
To learn more about managing database migrations with Flask and Lakebase Postgres, check out the Managing database migrations and schema changes with Flask and Lakebase Postgres guide.
Scalability considerations
As your Flask application grows, these strategies can improve performance and scalability:
-
Connection pooling is a technique used to manage database connections efficiently. Instead of opening and closing a new connection for each database operation, a pool of reusable connections is maintained.
Lakebase Postgres supports connection pooling through PgBouncer, with up to 10,000 pooled connections per compute. Pooling reduces the overhead of creating new connections.
To use connection pooling on Neon, switch to the pooled connection string, whose hostname includes a
-poolersuffix:# Update your DATABASE_URL to use the pooled connection string app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://user:password@pooler.address:5432/database'See Connection pooling for details.
-
For performance optimization, consider caching frequently accessed data. Caching reduces the load on your database and speeds up response times for users.
Implement caching for frequently accessed data using Flask-Caching:
from flask_caching import Cache cache = Cache() def create_app(): # ... previous code ... cache.init_app(app, config={'CACHE_TYPE': 'simple'}) return app @user_bp.route('/api/users', methods=['GET']) @cache.cached(timeout=60) # Cache for 60 seconds def get_users_api(): users = User.query.all() return jsonify([user.to_dict() for user in users])This example uses in-memory caching ('simple'). For production, you can switch to a caching backend like Redis.
-
Asynchronous processing allows your application to handle time-consuming tasks without blocking the main request-response cycle. You can use Celery, a distributed task queue, to run background tasks asynchronously.
To integrate Celery with Flask, you need to install the
celerypackage and configure it in your Flask application:pip install celeryfrom celery import Celery celery = Celery(__name__) def create_app(): # ... previous code ... celery.conf.update(app.config) return app @celery.task def send_email(user_id): user = User.query.get(user_id) # Send email to user # To call the task send_email.delay(user_id)This example defines a Celery task to send an email to a user asynchronously. You can run Celery workers to process these tasks in the background.
-
Rate limiting helps prevent abuse of your API and ensures fair usage among clients. This matters most for public APIs.
Implement rate limiting using Flask-Limiter:
from flask_limiter import Limiter from flask_limiter.util import get_remote_address limiter = Limiter(key_func=get_remote_address) def create_app(): # ... previous code ... limiter.init_app(app) return app @user_bp.route('/api/users', methods=['GET']) @limiter.limit("5 per minute") def get_users_api(): users = User.query.all() return jsonify([user.to_dict() for user in users])This example limits the
/api/usersendpoint to 5 requests per minute per IP address. You can adjust the limit based on your application's needs and resources.
Conclusion
You set up a Flask application on Lakebase Postgres with blueprints, Flask-Migrate, and a Tailwind CSS frontend.
As a next step, add authentication, authorization, and error handling, then add tests. You can run those tests against a Neon branch to try new features in isolation before deploying them to production.








