Build a Portfolio with Flask
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
Initialize Flask project
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.
Configure VS Code with Python
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).
Flask, Flask-SQLAlchemy
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
Configure PostgreSQL/SQLite
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.
Create Project, Skill models
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.
Set up Jinja2 templates
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
Create home, projects, about, contact routes
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.Build API endpoints for projects
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.
Build all portfolio sections
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.
Implement email sending
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.
Configure Gunicorn
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.
Image Upload & Project Gallery
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
Configure pytest
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.
Test homepage renders
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.Test all routes work
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
Deploy to Heroku/Railway
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.
