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