Skip to main content

Flask, Django, and Deployment

Overview​

In this vitamin, you'll build a Quote of the Day application using both Flask and Django, then deploy your Django app to Vercel.

  • Flask → Build a JSON API (test locally, submit to Gradescope)
  • Django → Build a full app with MVT (Model-View-Template), then deploy
Why Both Frameworks?

Flask and Django are both Python web frameworks. In the real world, you'd pick one. We're building both so you can compare:

  • Flask - Minimal, you add only what you need
  • Django - Full-featured with built-in ORM, admin panel, and templating

Django includes its own frontend via templates, so it deploys as one complete app!

Learning Objectives​

  • Build a REST API with Flask
  • Build a full web app with Django's Model-View-Template pattern
  • Understand the difference between Flask and Django
  • Deploy a Django app to Vercel

Part 1: Project Setup​

quote-app/
├── flask-backend/
└── django-app/
mkdir quote-app && cd quote-app
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate

Part 2: Flask Backend​

Flask is great for simple APIs. You'll build JSON endpoints and test them locally.

Step 1: Setup​

mkdir flask-backend && cd flask-backend
pip install flask

Step 2: Create the API​

Create flask-backend/app.py:

from flask import Flask, jsonify, request
import random

app = Flask(__name__)

quotes = [
{"id": 1, "text": "The only way to do great work is to love what you do.", "author": "Steve Jobs"},
{"id": 2, "text": "Code is like humor. When you have to explain it, it's bad.", "author": "Cory House"},
{"id": 3, "text": "First, solve the problem. Then, write the code.", "author": "John Johnson"},
{"id": 4, "text": "Simplicity is the soul of efficiency.", "author": "Austin Freeman"},
{"id": 5, "text": "Fix the cause, not the symptom.", "author": "Steve Maguire"},
]

next_id = 6 # For generating new IDs

# TODO 1: Create GET /api/quote - return a random quote as JSON

# TODO 2: Create GET /api/quotes - return all quotes as JSON

# TODO 3: Create POST /api/quotes - add a new quote
# - Get JSON data from request.json
# - Create new quote with next_id, text, and author
# - Append to quotes list
# - Increment next_id
# - Return the new quote with status 201

# TODO 4: Create PUT /api/quotes/<id> - update a quote by id
# - Get JSON data from request.json
# - Find quote with matching id
# - Update text and/or author
# - Return the updated quote
# - If not found, return error with status 404

# TODO 5: Create DELETE /api/quotes/<id> - delete a quote by id
# - Find and remove quote with matching id
# - Return success message
# - If not found, return error with status 404

if __name__ == "__main__":
app.run(debug=True, port=5000)

Step 3: Test Locally​

python app.py

Test GET in browser:

  • http://localhost:5000/api/quote - random quote
  • http://localhost:5000/api/quotes - all quotes

Test POST, PUT, DELETE with curl or Postman:

# Add a new quote (POST)
curl -X POST http://localhost:5000/api/quotes \
-H "Content-Type: application/json" \
-d '{"text": "New quote here", "author": "Someone"}'

# Update a quote (PUT)
curl -X PUT http://localhost:5000/api/quotes/1 \
-H "Content-Type: application/json" \
-d '{"text": "Updated quote", "author": "New Author"}'

# Delete a quote (DELETE)
curl -X DELETE http://localhost:5000/api/quotes/1

Step 4: Save Dependencies​

pip freeze > requirements.txt

Part 3: Django App with MVT​

Django uses the Model-View-Template (MVT) pattern. Unlike Flask, Django includes its own frontend via templates, making it a complete web app.

ComponentWhat it does
ModelDefines data structure (database tables)
ViewHandles requests, returns responses
TemplateHTML pages with Django syntax

Step 1: Setup​

cd .. # back to quote-app
pip install django
django-admin startproject quotesite django-app
cd django-app
python manage.py startapp quotes

Step 2: Configure Settings​

Update quotesite/settings.py:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'quotes', # Add this
]

Also add at the bottom:

# For deployment
ALLOWED_HOSTS = ['*']

Step 3: Create the Model​

Update quotes/models.py:

from django.db import models

# TODO 6: Create a Quote model with:
# - text (CharField, max_length=500)
# - author (CharField, max_length=100)
# - __str__ method that returns the text

class Quote(models.Model):
# Your fields here
pass

Run migrations to create the database table:

python manage.py makemigrations

This looks at your models and creates a migration file (like a "recipe" for database changes). You should see:

Migrations for 'quotes':
quotes/migrations/0001_initial.py
- Create model Quote

Then apply the migrations to create the actual database tables:

python manage.py migrate

This applies all migrations to actually create the tables in the database. You should see:

Operations to perform:
Apply all migrations: admin, auth, contenttypes, quotes, sessions
Running migrations:
Applying quotes.0001_initial... OK

Step 4: Add Data via Admin​

Update quotes/admin.py:

from django.contrib import admin
from .models import Quote

admin.site.register(Quote)
python manage.py createsuperuser
python manage.py runserver

Go to http://localhost:8000/admin and add at least 5 quotes.

Step 5: Create the Views​

Update quotes/views.py:

from django.shortcuts import render
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from .models import Quote
import random
import json

# ============ TEMPLATE VIEWS (return HTML) ============

# TODO 7: Create a home view
# - Get all quotes from database: Quote.objects.all()
# - Pick a random quote
# - Render 'quotes/home.html' with quote in context

# TODO 8: Create an all_quotes view
# - Get all quotes
# - Render 'quotes/all_quotes.html' with quotes in context


# ============ API VIEWS (return JSON) ============

# TODO 9: Create a random_quote_api view
# - Get all quotes, pick random one
# - Return JsonResponse with 'id', 'text', 'author'

@csrf_exempt # Allows POST/PUT/DELETE without CSRF token
def quotes_api(request):
# TODO 10: Handle GET and POST for /api/quotes/
# GET: Return all quotes as JSON (use safe=False for lists)
# POST: Create new quote from request.body, return with status 201
# Check request.method to determine which action
pass

@csrf_exempt
def quote_detail_api(request, id):
# TODO 11: Handle GET, PUT, DELETE for /api/quotes/<id>/
# GET: Return single quote as JSON (404 if not found)
# PUT: Update quote from request.body, return updated quote
# DELETE: Delete quote, return success message
# Check request.method to determine which action
pass
RESTful API Design

Same URL, different HTTP methods:

  • GET /api/quotes/ → list all
  • POST /api/quotes/ → create new
  • GET /api/quotes/1/ → get one
  • PUT /api/quotes/1/ → update one
  • DELETE /api/quotes/1/ → delete one

Use request.method to check which HTTP method was used.

@csrf_exempt

Django has CSRF protection by default, which blocks POST/PUT/DELETE requests without a token. For APIs, we use @csrf_exempt to disable this. In production, you'd use proper authentication instead.

Step 6: Create the Templates​

mkdir -p quotes/templates/quotes

Create quotes/templates/quotes/home.html:

<!DOCTYPE html>
<html>
<head>
<title>Quote of the Day</title>
<style>
body {
font-family: Georgia, serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
text-align: center;
}
.quote-box {
background: #f5f5f5;
padding: 30px;
border-radius: 10px;
margin: 30px 0;
}
.text { font-size: 22px; font-style: italic; line-height: 1.6; }
.author { color: #666; margin-top: 15px; }
a { color: #007bff; text-decoration: none; margin: 0 10px; }
</style>
</head>
<body>
<h1>Quote of the Day</h1>

<div class="quote-box">
<!-- TODO 15: Display quote.text and quote.author using {{ }} -->
<p class="text">"..."</p>
<p class="author">- ...</p>
</div>

<a href="{% url 'home' %}">New Quote</a>
<a href="{% url 'all_quotes' %}">All Quotes</a>
</body>
</html>

Create quotes/templates/quotes/all_quotes.html:

<!DOCTYPE html>
<html>
<head>
<title>All Quotes</title>
<style>
body {
font-family: Georgia, serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.quote-card {
background: #f5f5f5;
padding: 20px;
margin: 15px 0;
border-radius: 8px;
border-left: 4px solid #007bff;
}
.text { font-style: italic; }
.author { color: #666; margin-top: 10px; }
a { color: #007bff; text-decoration: none; }
</style>
</head>
<body>
<h1>All Quotes</h1>

<!-- TODO 16: Loop through quotes with {% for quote in quotes %} -->
<div class="quote-card">
<p class="text">"Quote text"</p>
<p class="author">- Author</p>
</div>
<!-- {% endfor %} -->

<p><a href="{% url 'home' %}">← Random Quote</a></p>
</body>
</html>

Step 7: Set Up URLs​

All our API routes start with api/ (like api/quote/, api/quotes/, etc.). Instead of repeating api/ in every pattern, use include() to group them in a separate file called quotes/api_urls.py.

Create quotes/urls.py:

from django.urls import path, include
from . import views

urlpatterns = [
# TODO 12: Add template view patterns
# - '' -> home view, name='home'
# - 'all/' -> all_quotes view, name='all_quotes'

# TODO 13: Use include() to add all API routes under 'api/'
]

Create quotes/api_urls.py:

from django.urls import path
from . import views

# TODO 14: Define API URL patterns (no 'api/' prefix needed - include() adds it)
# - 'quote/' -> random_quote_api (random quote)
# - 'quotes/' -> quotes_api (GET all, POST new)
# - 'quotes/<int:id>/' -> quote_detail_api (GET one, PUT, DELETE)

urlpatterns = [
# Your patterns here
]

Update quotesite/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('quotes.urls')),
]

path('', include('quotes.urls')) means: take all URL patterns from quotes/urls.py and add them starting at the root (''). So if quotes/urls.py has path('all/', ...), the full URL becomes /all/.

Step 8: Test Locally​

python manage.py runserver
  • http://localhost:8000/ - Random quote page
  • http://localhost:8000/all/ - All quotes page
  • http://localhost:8000/admin/ - Admin panel

Part 4: Deploy Django to Vercel​

One Deployment vs Two

If you were using Flask + React, you'd need to deploy twice:

  1. Deploy Flask backend to Vercel/Render
  2. Deploy React frontend to Vercel
  3. Configure React to call the Flask API URL

But with Django + Templates, the frontend is built into Django, so you only deploy once! This is one of Django's advantages for full-stack apps.

Since Django includes templates (the frontend), you deploy everything together as one app.

Step 1: Prepare for Deployment​

Create django-app/vercel.json:

{
"builds": [{
"src": "quotesite/wsgi.py",
"use": "@vercel/python",
"config": { "maxLambdaSize": "15mb" }
}],
"routes": [
{ "src": "/(.*)", "dest": "quotesite/wsgi.py" }
]
}

Create django-app/requirements.txt:

django

Update quotesite/wsgi.py:

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'quotesite.settings')

application = get_wsgi_application()
app = application # Vercel needs this

Step 2: Push to GitHub​

Important

Push only the django-app/ folder as its own repository. Do NOT include the Flask backend - that's a separate project for Gradescope submission only.

  1. Create a new GitHub repository (e.g., "quote-django")
  2. Navigate into the django-app/ folder:
    cd django-app
    git init
    git add .
    git commit -m "Initial commit"
    git remote add origin https://github.com/YOUR_USERNAME/quote-django.git
    git push -u origin main

Step 3: Deploy on Vercel​

  1. Go to vercel.com and sign in with GitHub
  2. Click "Add New Project"
  3. Import your Django repository
  4. Click Deploy
  5. Wait for deployment to complete

Step 4: Test Your Deployed App​

Visit your Vercel URL:

  • https://your-app.vercel.app/ - Random quote
  • https://your-app.vercel.app/all/ - All quotes
Automatic Redeployment

Every time you push changes to GitHub, Vercel automatically redeploys your app. If you create a Pull Request, Vercel creates a preview deployment so you can test changes before merging!

Note on Database

SQLite resets on each Vercel deploy. We'll use a data migration to automatically populate quotes whenever the database is created.

Step 5: Create a Data Migration​

A data migration runs during migrate and inserts initial data into the database.

python manage.py makemigrations quotes --empty --name seed_quotes

This creates an empty migration file. Open quotes/migrations/0002_seed_quotes.py and update it:

from django.db import migrations

def seed_quotes(apps, schema_editor):
Quote = apps.get_model('quotes', 'Quote')
quotes = [
{"text": "The only way to do great work is to love what you do.", "author": "Steve Jobs"},
{"text": "Code is like humor. When you have to explain it, it's bad.", "author": "Cory House"},
{"text": "First, solve the problem. Then, write the code.", "author": "John Johnson"},
{"text": "Simplicity is the soul of efficiency.", "author": "Austin Freeman"},
{"text": "Fix the cause, not the symptom.", "author": "Steve Maguire"},
]
for q in quotes:
Quote.objects.create(text=q["text"], author=q["author"])

class Migration(migrations.Migration):
dependencies = [
('quotes', '0001_initial'),
]

operations = [
migrations.RunPython(seed_quotes),
]

Now whenever migrate runs (including on Vercel), the quotes are automatically added!


Flask vs Django Summary​

FlaskDjango
SetupMinimalMore boilerplate
DataHardcoded listDatabase with ORM
FrontendNone (API only)Built-in templates
Admin PanelNoneBuilt-in
DeploymentAPI onlyFull app (frontend + backend)
Best forSimple APIsComplete web apps

Submission​

Submit to Gradescope:

  • flask-backend/ folder (with app.py and requirements.txt)
  • django-app/ folder (entire Django project)
  • Your deployed Django URL
Do NOT include
  • venv/ folder
  • db.sqlite3 (database file)
  • __pycache__/ folders