Build a Portfolio with Flask

Stack: Python + Flask + Jinja2

Flask Portfolio Tutorial: Build a Python-Powered Professional Website

Master Flask by building a complete portfolio website from scratch. This hands-on tutorial teaches Flask fundamentals including routing, Jinja2 templating, SQLAlchemy ORM, and deployment. Perfect for Python developers who want full control over their web applications without framework overhead.

What You'll Build

  • Professional portfolio showcase with project galleries
  • Admin interface for content management
  • Contact form with email integration
  • Image upload and optimization system
  • Responsive design with Jinja2 templates

Flask Concepts You'll Master

  • Flask Routing: Build URL routes and handle requests
  • SQLAlchemy ORM: Create database models and relationships
  • Jinja2 Templates: Build dynamic HTML with template inheritance
  • Flask-Login: Implement secure authentication
  • Flask-WTF: Handle forms with validation and CSRF protection
  • Flask Blueprints: Organize your application modularly
  • Deployment: Deploy Flask apps to production servers

Prerequisites

  • Python 3.8+ programming knowledge
  • Basic understanding of HTML, CSS, and JavaScript
  • Familiarity with virtual environments (venv)
  • PostgreSQL or SQLite database
  • Code editor with Python support

Time Commitment: 5-7 hours total. Work at your own pace with AI assistance. Each step includes copyable prompts for Claude or ChatGPT.

Why Flask for Portfolio Development?

Flask's microframework approach gives you complete control without unnecessary complexity. Unlike heavyweight frameworks, Flask lets you choose exactly what your portfolio needsβ€”no bloat, no magic. This minimalism makes Flask perfect for portfolios that need to be fast, maintainable, and easy to understand. Companies like Netflix, Reddit, and Airbnb use Flask in production.

This tutorial teaches Pythonic web development with clean architecture patterns. You'll learn to structure Flask applications professionally, implement security best practices, and deploy with confidence. Learn more in our Flask framework deep dive.

Build a Portfolio with Flask

Flask brings Python's elegance to web development in the most minimalist way possible. Building a portfolio with Flask means you control every detail - from routing to templates to database design - without the framework making decisions for you. You will craft a lightweight, fast-loading portfolio that showcases not just your projects, but your understanding of web fundamentals. With Jinja2 templates and SQLAlchemy, you will create something clean, maintainable, and distinctly yours - perfect for data scientists, backend engineers, or anyone who appreciates Python's philosophy of simplicity.

Flask Portfolio Setup: Installation and Project Configuration

1

Initialize Flask project

You will initialize a Flask project with a virtual environment - application factory pattern keeps things clean and scalable.
AI Prompt
Initialize a new Flask portfolio project

Create a project directory, set up a virtual environment with python -m venv venv, activate it (source venv/bin/activate on Unix or venvScriptsactivate on Windows)

Install Flask with pip install Flask

Create the project structure: app/ folder for application code with __init__.py (application factory), templates/ for Jinja2 templates, static/ for CSS/JS/images (with subdirectories css/, js/, images/), instance/ for config files

Create config.py for configuration classes (Development, Production, Testing)

Set up requirements.txt to track dependencies

Initialize git repository with appropriate .gitignore for Python/Flask projects

Create .env file for environment variables using python-dotenv.
2

Configure VS Code with Python

You will configure VS Code with Python extensions for autocomplete and debugging - Jinja2 highlighting makes templates readable.
AI Prompt
Configure VS Code for Flask portfolio development

Install Python extension by Microsoft, Pylance for enhanced IntelliSense, Jinja template highlighting extension for template syntax support

Create .vscode/settings.json with Python interpreter path pointing to your venv, enable pylint or flake8 for linting, configure Black or autopep8 for code formatting (PEP 8 compliance)

Set up launch.json for Flask debugging with FLASK_APP pointing to your application factory and FLASK_ENV=development

Add .env file support

Configure editor to use 4-space indentation (PEP 8 standard)

Install additional useful extensions: Python Test Explorer for pytest integration, Better Jinja for template editing

Set up code snippets for common Flask patterns (routes, templates, forms).
3

Flask, Flask-SQLAlchemy

You will install Flask extensions like SQLAlchemy for databases and Flask-WTF for forms - powerful features without bloat.
AI Prompt
Install essential Flask extensions for portfolio functionality

Run pip install Flask Flask-SQLAlchemy Flask-Migrate for database management

Add python-dotenv for environment variables, Flask-WTF for forms with CSRF protection

Install Pillow for image processing if handling image uploads

Add python-slugify for generating URL-friendly slugs from project titles

For email functionality, install Flask-Mail

Consider Flask-Assets for asset management and minification

Update requirements.txt with pip freeze > requirements.txt

Configure each extension in your Flask app factory within app/__init__.py: initialize db, migrate, and other extensions with app.config settings

Set up configuration classes in config.py with different settings for development, testing, and production environments (SECRET_KEY, SQLALCHEMY_DATABASE_URI, etc).

Database and Environment Configuration for Flask

4

Configure PostgreSQL/SQLite

You will configure SQLite for development and PostgreSQL for production - different configs make deployment smooth.
AI Prompt
Configure database for portfolio data storage

For development, use SQLite with SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(basedir, "instance/portfolio.db")

For production, use PostgreSQL with connection string format "postgresql://username:password@localhost/dbname" stored in environment variable

Create config.py with different configuration classes (Config, DevelopmentConfig, ProductionConfig, TestingConfig) each with appropriate database URIs. Set SQLALCHEMY_TRACK_MODIFICATIONS = False to suppress warnings and improve performance

Configure database location in instance/ folder for SQLite to keep it out of version control

Initialize Flask-Migrate with flask db init to create migrations folder

Set up environment variables in .env file for sensitive credentials (SECRET_KEY, DATABASE_URL for production)

Test database connection after configuration.
5

Create Project, Skill models

You will create SQLAlchemy models for Projects and Skills - class methods handle queries and slugs generate automatically.
AI Prompt
Create SQLAlchemy models in app/models.py for portfolio data

Define Project model with fields: id (Integer, primary_key), title (String, unique, not null), slug (String, unique, not null), description (Text, short description), full_description (Text, detailed description), tech_stack (Text or JSON, technologies used), project_url (String, nullable, live demo link), github_url (String, nullable, source code link), image_path (String, project thumbnail), featured (Boolean, default False), display_order (Integer), created_at (DateTime, default=datetime.utcnow), updated_at (DateTime, onupdate=datetime.utcnow)

Add __repr__ method for debugging

Implement slug generation from title using python-slugify in a method or property

Define Skill model with fields: id, name, category (frontend/backend/tools/design), proficiency_level (beginner/intermediate/advanced/expert), icon_class (String, for Font Awesome or similar icons), display_order

Create association table for many-to-many relationship between Projects and Skills

Add relationships in models: Project has skills relationship, Skill has projects relationship

Create class methods for common queries like Project.get_featured(), Project.get_published()

Add property methods for formatted dates or other computed attributes.
6

Set up Jinja2 templates

You will set up Jinja2 templates with inheritance - macros create reusable components, Bootstrap or custom CSS for styling.
AI Prompt
Set up Jinja2 template structure in templates/ directory

Create base.html with complete HTML5 boilerplate, meta tags (charset, viewport, description), link to CSS files (Bootstrap 5 CDN or custom CSS), navigation bar with links to sections (Home, Projects, About, Contact), {% block content %}{% endblock %} for page content, footer with social media links and copyright, and script tags for JavaScript

Build layout structure with Jinja2 template inheritance

Create templates for each section: index.html (homepage with hero, featured projects, CTA), projects.html (all projects grid), project_detail.html (single project view), about.html (bio and skills), contact.html (contact form)

Set up static folder structure with css/, js/, images/ subdirectories

Create static/css/style.css for custom styles, color scheme, typography, responsive design

Use CSS Grid or Flexbox for layouts

Create Jinja2 macros in templates/macros.html for reusable components: project_card(project), skill_badge(skill), nav_link(text, url)

Configure Flask to serve static files correctly

Add CSS transitions and hover effects for better UX.

Building Portfolio Features: Core Functionality and Admin Panel

7

Create home, projects, about, contact routes

You will create Flask routes with decorators mapping URLs to functions - query the database and pass data to templates cleanly.
AI Prompt
Create Flask routes for portfolio pages in app/routes.py or use blueprints for better organization

Define index route (@app.route("/")) that queries featured projects from database, renders homepage with hero section and featured projects

Create projects route (@app.route("/projects")) that gets all projects ordered by display_order, optionally filters by technology or category, renders projects grid with pagination if needed

Build project detail route (@app.route("/project/<slug>")) that queries single project by slug, handles 404 if not found, renders detailed project view

Define about route (@app.route("/about")) that queries skills grouped by category, renders about page

Create contact route with GET and POST methods (@app.route("/contact", methods=["GET", "POST"])) for contact form

Implement proper error handling with @app.errorhandler(404) and @app.errorhandler(500) to show custom error pages

Add URL generation using url_for() in templates

Consider using Flask blueprints to organize routes: create app/main/ blueprint for public routes and app/api/ blueprint if building API endpoints.
8

Build API endpoints for projects

You will build REST API endpoints returning JSON for projects and skills - filtering and pagination show RESTful design.
AI Prompt
Build RESTful API endpoints for portfolio data

Create app/api/ blueprint for API routes

Implement GET /api/projects endpoint that returns JSON list of all projects with jsonify(), includes filtering by technology/category via query parameters, implements pagination with page and per_page parameters

Create GET /api/projects/<id> endpoint for single project details

Add GET /api/skills endpoint returning all skills grouped by category

Implement proper HTTP status codes: 200 for success, 404 for not found, 500 for server error

Add CORS support if API will be consumed by external applications using flask-cors package

Implement JSON schemas for consistent API responses with fields: id, title, slug, description, tech_stack, project_url, github_url, image_url, created_at

Add error handling that returns JSON errors

Consider implementing API versioning (/api/v1/projects)

Add rate limiting if exposing publicly using Flask-Limiter

Test API endpoints with curl or Postman. Document API endpoints with clear request/response examples.
9

Build all portfolio sections

You will bring everything together with routes, models, and templates - homepage, projects, project detail, and about page.
AI Prompt
Build complete portfolio page implementations in Flask

For Homepage: create index view function that queries featured projects with Project.query.filter_by(featured=True).order_by(Project.display_order).all(), passes to template, renders hero section with name/title/tagline, showcases featured projects in grid/carousel, includes skills summary, adds call-to-action buttons

For Projects page: query all projects with pagination, implement filtering by technology (use query parameters), add sorting options, render project cards with images/titles/descriptions/tech badges

For Project Detail: query project by slug with Project.query.filter_by(slug=slug).first_or_404(), render full description, tech stack details, project images/screenshots, links to live demo and GitHub, related projects section

For About page: render professional bio, display skills organized by category using Skill.query.order_by(Skill.category, Skill.display_order).all(), show education/experience timeline, include downloadable resume link

Add smooth scrolling between sections using JavaScript

Implement responsive design with mobile-first approach

Add loading states and transitions for better UX.
10

Implement email sending

You will implement a contact form with Flask-WTF validation and Flask-Mail for emails - CSRF and rate limiting stop spam.
AI Prompt
Implement functional contact form with email sending capability

Create ContactForm class using Flask-WTF in app/forms.py with fields: name (StringField, validators=[DataRequired(), Length(min=2, max=100)]), email (EmailField, validators=[DataRequired(), Email()]), subject (StringField, optional), message (TextAreaField, validators=[DataRequired(), Length(min=10)])

In contact route, instantiate form, validate on POST with form.validate_on_submit()

Configure Flask-Mail in app config: set MAIL_SERVER (smtp.gmail.com), MAIL_PORT (587), MAIL_USE_TLS (True), MAIL_USERNAME, MAIL_PASSWORD (from environment variables)

Create send_email function that constructs Message object with sender, recipients, subject, body (plain text and HTML versions)

Handle form submission: validate inputs, send email using mail.send(msg), flash success message, redirect to prevent resubmission

Add error handling for email sending failures

Create email template in templates/email/contact_notification.html for formatted email body

Implement spam prevention: add honeypot field (hidden field that bots fill), implement rate limiting with Flask-Limiter

Store contact form submissions in database optionally for record keeping

Add client-side validation for better UX.
11

Configure Gunicorn

You will prep for deployment with Gunicorn and a Procfile - environment configs and security headers make it production-ready.
AI Prompt
Prepare Flask portfolio for production deployment

Install Gunicorn WSGI server: pip install gunicorn

Create Procfile for platforms like Heroku with content: web: gunicorn "app:create_app()"

If using application factory pattern, ensure it works with Gunicorn

Create runtime.txt specifying Python version (python-3.11.0)

Ensure all dependencies are in requirements.txt with correct versions

Set up environment-specific configuration: create ProductionConfig class with DEBUG=False, proper SECRET_KEY from environment, production database URL

Implement proper logging: configure Flask app.logger for production, log to file or external service

Add security headers: set X-Content-Type-Options, X-Frame-Options, Content-Security-Policy in response headers

Implement database migrations strategy: ensure flask db upgrade runs in deployment process

Configure static file serving: for production, consider using CDN or configuring web server (Nginx) to serve static files

Test production configuration locally using: FLASK_ENV=production gunicorn "app:create_app()"

Create deployment checklist: set SECRET_KEY, configure DATABASE_URL, set MAIL_* credentials, disable debug mode, configure allowed hosts if applicable.
15

Image Upload & Project Gallery

You will implement image upload for project screenshots with Pillow for resizing and optimization - multiple images per project with gallery display.
AI Prompt
Add comprehensive image handling for portfolio project screenshots and galleries.

Install Pillow for image processing: pip install Pillow.

Configure file upload settings in config.py: UPLOAD_FOLDER = 'static/uploads/projects', MAX_CONTENT_LENGTH = 5 * 1024 * 1024 (5MB limit), ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}.

Create upload directory structure organizing hero images, project screenshots, thumbnails.

Implement image upload in admin panel: create upload route that validates file type and size using allowed_file() helper function, generates unique filenames with uuid or timestamp to prevent collisions, saves original file to uploads folder.

Use Pillow to create optimized versions: resize large images to maximum dimensions (e.g., 1200px width), create thumbnail versions (400px, 800px) for different display contexts, maintain aspect ratio, compress images with quality=85 for JPG, convert to WebP format with fallback for better compression.

Update Project model for multiple images: change image_path to TEXT or JSON for storing array of image paths, add methods get_thumbnail(), get_fullsize(), get_gallery_images().

Create ProjectImage table for proper many-to-many relationship: fields include project_id, image_path, thumbnail_path, caption, display_order, is_primary.

Build image gallery display in templates: create project detail template with image carousel/slider using JavaScript library (Swiper, Slick) or custom implementation, implement lightbox for full-size image viewing (PhotoSwipe, GLightbox), add image captions and credits.

For hero section images: implement background image upload for hero section, add overlay gradient controls, allow positioning adjustments (top, center, bottom).

Add image management in admin: multi-file upload interface with drag-and-drop using Dropzone.js or similar, image preview before upload, drag-to-reorder gallery images, delete/replace functionality with Storage cleanup.

Implement image deletion: use os.remove() or Storage facade to delete files when projects are deleted or images replaced, clean up orphaned images periodically.

Add responsive image serving: use srcset in templates for different screen sizes, serve WebP with JPG/PNG fallback using <picture> element.

Testing Your Flask Portfolio Application

12

Configure pytest

You will set up pytest with fixtures - in-memory SQLite keeps tests fast and coverage reports show what's tested.
AI Prompt
Set up comprehensive testing with pytest for Flask portfolio

Install testing packages: pip install pytest pytest-flask pytest-cov

Create tests/ directory with __init__.py, conftest.py for fixtures, and test files (test_routes.py, test_models.py, test_forms.py)

In conftest.py, create pytest fixtures: app fixture that creates Flask app with testing config, client fixture for test client (app.test_client()), init_database fixture that creates database tables and tears them down after tests

Configure test database to use SQLite in-memory (sqlite:///:memory:) or separate test database file. Set app.config["TESTING"] = True, WTF_CSRF_ENABLED = False to disable CSRF for testing

Create pytest.ini for pytest configuration with test paths and options

Add test command to Makefile or document in README

Create factory functions or fixtures for creating test data: create_test_project(), create_test_skill()

Set up coverage reporting with pytest --cov=app --cov-report=html to track code coverage

Create .coveragerc to exclude certain files from coverage

Plan testing strategy: test routes return correct status codes, test database models, test form validation, test API endpoints return correct JSON.
13

Test homepage renders

You will write your first test to verify the homepage renders - check status code and content to validate the stack.
AI Prompt
Write your first test for the portfolio homepage in tests/test_routes.py. Import necessary modules: pytest, your Flask app, test client from fixtures

Define test function test_homepage_loads(client) that uses client fixture

Test that GET request to "/" returns 200 status code with response = client.get("/"), assert response.status_code == 200

Test that response contains expected HTML elements: assert b"Your Name" in response.data or use response.get_data(as_text=True) with assert "Your Name" in response

Test that featured projects display: create test projects using database session, set featured=True, query homepage, assert project titles appear in response

Test navigation links exist: assert "Projects" in response, assert "About" in response, assert "Contact" in response

Run tests with pytest -v in terminal and verify all assertions pass

Add more specific tests: test page title is correct, test hero section content, test CTA buttons exist. This validates your basic routing and template rendering, establishing a testing foundation.
14

Test all routes work

You will write comprehensive tests for all routes and API endpoints - mock email sending and test success and error cases.
AI Prompt
Write comprehensive route tests for all portfolio endpoints in tests/test_routes.py

Test projects page: test GET /projects returns 200, displays all projects, shows project titles and descriptions, pagination works if implemented, filtering by technology works with query parameters

Test project detail page: test GET /project/<slug> with valid slug returns 200 and correct project, test invalid slug returns 404, test full description displays, test tech stack and links present

Test about page: test GET /about returns 200, bio content displays, skills are listed and grouped by category

Test contact page: test GET /contact shows form, test POST with valid data sends email and shows success message (mock email sending with unittest.mock), test POST with invalid data shows error messages and does not send email, test CSRF token validation if enabled. Test API endpoints if implemented: test /api/projects returns JSON with correct structure, test /api/projects/<id> returns single project JSON, test 404 for invalid ID. Use test client for all requests. Create helper functions for common operations: create_test_project(client, **kwargs). Test both authenticated and unauthenticated access if you have admin features. Verify error handlers: test 404 page shows for invalid URL, test 500 error handling with intentional error.

Deploying Your Flask Portfolio to Production

16

Deploy to Heroku/Railway

You will deploy to Heroku or Railway where environment variables and SSL are handled - push code and go live.
AI Prompt
Deploy Flask portfolio to production hosting

For Heroku: install Heroku CLI, create Heroku app with heroku create app-name, ensure Procfile exists with "web: gunicorn app:app" or "web: gunicorn 'app:create_app()'", ensure runtime.txt specifies Python version, commit all changes to git. Set environment variables in Heroku dashboard or CLI: heroku config:set SECRET_KEY=your-secret-key, DATABASE_URL (Heroku Postgres addon), FLASK_ENV=production, MAIL_SERVER, MAIL_USERNAME, MAIL_PASSWORD. Add Heroku Postgres addon: heroku addons:create heroku-postgresql:hobby-dev. Configure DATABASE_URL in config to use Heroku's postgres:// URL (may need to convert to postgresql://). Deploy with git push heroku main. Run database migrations: heroku run flask db upgrade. For Railway: connect GitHub repository, configure build command (pip install -r requirements.txt), configure start command (gunicorn app:app), set environment variables in Railway dashboard, connect PostgreSQL database. For both platforms: test deployed site thoroughly - test all pages load, test contact form sends emails, verify database works, check error handling. Set up custom domain and configure DNS. Configure SSL/HTTPS (automatic on Heroku/Railway). Set up monitoring and logging: use Heroku logs (heroku logs --tail) or Railway logs. Configure error tracking with Sentry or similar service. Set up database backups. Test performance and optimize if needed. Add deployment documentation to README.