Introduction
I'm typically a skeptic when it comes to the fantastical and the mysterious. For years, I dismissed the notion of Sasquatch as ludicrous. Growing up in Colorado, where forests are commonplace, the idea of a giant ape residing in such areas seemed far-fetched. However, my perspective changed in early 2022, when my family and I relocated to the Pacific Northwest and were quickly awed by the forests of the Pacific Northwest. They are truly a spectacle, with their overwhelming density and scale dwarfing even the expansive wilderness of Colorado. Within a short span of time, we encountered numerous unexplained phenomena within these forests that turned both me and my wife into believers. When I sought out similar experiences online, I found a plethora of resources, although many were poorly organized and outdated.
Fast forward to 2023, I am nearing the completion of an intensive Full-Stack Web Development bootcamp at the Flatiron School. As part of the program, I've been tasked to develop a full-stack application using Python Flask for the backend, and React for the frontend and after months of working on Facebook, Instagram, and Snapchat clones, why not branch out and have fun with something a little different. Not lacking in ambition, I started out with the goal of making an app that was better than the outdated and hard to navigate apps that were currently available. I don't want this to be a summary of all my code and would rather share my journey through creating this app, some of the technologies I used, and what I learned.
Starting Point
To reinforce my grasp of the technologies involved, I chose to develop the project solo. In contrast to previous group projects, I wanted to approach this one in a more thoughtful and methodical manner. My natural instinct was to map out my backend and then build my frontend to interact with it. After careful planning and mapping out how I wanted my app's models to interact and relate, I started by setting up the Flask backend and React frontend in separate directories: "server" and "client."
Configuring the Flask App
Within the "server" directory, I created an app.py
file to configure the Flask application. The create_app()
function played a vital role, where I set up CORS, secret key, SQLite database URI, and Flask extensions such as SQLAlchemy and Flask-Migrate. Additionally, I defined routes and API endpoints using Flask-RESTful. The example below shows the configuration along with one of the routes that was used.
from flask import Flask, request, session, make_response, jsonify
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_restful import Api, Resource
from flask_bcrypt import check_password_hash, generate_password_hash
from dotenv import load_dotenv
import os
from datetime import datetime
from models import User, Location, Sighting, Comment
from extensions import db, migrate
# Load environment variables from .env
load_dotenv()
# Retrieve the secret key from the environment variable
secret_key = os.getenv("SECRET_KEY")
def create_app():
app = Flask(__name__)
CORS(app)
app.config["SECRET_KEY"] = secret_key
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.json.compact = False
db.init_app(app)
migrate.init_app(app, db)
return app
app = create_app()
api = Api(app)
class Login(Resource):
def post(self):
data = request.get_json()
email = data.get("email")
password = data.get("password")
user = User.query.filter_by(email=email).first()
if user and check_password_hash(user.password, password):
session["user_id"] = user.id
return make_response(jsonify(user.to_dict()), 200)
else:
return make_response({"error": "Invalid username or password"}, 401)
api.add_resource(Login, "/login")
Setting Up the Models
In the same "server" directory, I defined the database models: User, Location, Sighting, and Comment. These models represented the entities and relationships within the application. I utilized SQLAlchemy's ORM to establish relationships and create the necessary tables in the database. Each model had methods to convert instances to dictionaries, making it easier to work with JSON responses. I did use BCrypt to hash the passwords for each user. Below is an example of the model imports and one of the models.
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import validates
from sqlalchemy.ext.associationproxy import association_proxy
from flask_bcrypt import Bcrypt
import re
from datetime import datetime
from extensions import db
bcrypt = Bcrypt()
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(length=254), unique=True, nullable=False)
password = db.Column(db.String(length=254))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
sightings = db.relationship("Sighting", backref="user", lazy=True)
comments = db.relationship("Comment", backref="user", lazy=True)
locations = association_proxy("sightings", "location")
@validates("email")
def validate_email(self, key, email):
assert (
re.match(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", email)
is not None
), "Invalid email address"
return email
def set_password(self, password):
hashed_password = bcrypt.generate_password_hash(password)
self.password = hashed_password.decode("utf-8")
def verify_password(self, password):
return bcrypt.check_password_hash(self.password, password)
def to_dict(self):
return {
"id": self.id,
"email": self.email,
}
def __repr__(self):
return f"<User email={self.email}>"
Migrations and Seeding Data
To manage database schema changes, I used Flask-Migrate, which enabled easy migration creation and execution. I started with an initial migration and then used a seed.py
file to populate the tables with test data. The faker package helped generate realistic data for users, locations, sightings, and comments.
from faker import Faker
from random import choice
from models import User, Location, Sighting, Comment
from extensions import db
from app import app
fake = Faker()
def clear_data():
with app.app_context():
db.session.query(User).delete()
db.session.query(Location).delete()
db.session.query(Sighting).delete()
db.session.query(Comment).delete()
db.session.commit()
def seed_db(num_users=10, num_locations=5, num_sightings=20, num_comments=50):
with app.app_context():
for _ in range(num_users):
user = User(
email=fake.unique.email(),
)
user.set_password("abc123")
db.session.add(user)
db.session.commit()
# Get all users
users = User.query.all()
# Seed locations
for _ in range(num_locations):
location = Location(
name=fake.city(),
state=fake.state_abbr(),
description=fake.text(max_nb_chars=200),
)
db.session.add(location)
db.session.commit()
# Get all locations
locations = Location.query.all()
# Seed sightings
for _ in range(num_sightings):
sighting = Sighting(
user_id=choice(users).id,
location_id=choice(locations).id,
sighting_date=fake.date_this_year(),
sighting_time=fake.time_object(),
description=fake.text(max_nb_chars=1000),
created_at=fake.date_time_this_year(),
updated_at=fake.date_time_this_year(),
)
db.session.add(sighting)
db.session.commit()
# Get all sightings
sightings = Sighting.query.all()
# Seed comments
for _ in range(num_comments):
comment = Comment(
user_id=choice(users).id,
sighting_id=choice(sightings).id,
comment_text=fake.text(max_nb_chars=500),
created_at=fake.date_time_this_year(),
updated_at=fake.date_time_this_year(),
)
db.session.add(comment)
db.session.commit()
# Call the functions
clear_data()
seed_db()
Creating the Frontend with Tailwind CSS
To build the React frontend, I utilized Create React App. It quickly set up a functional frontend within the "client" directory. In this project, I incorporated Tailwind CSS—a utility-first CSS framework. Tailwind CSS allowed me to rapidly prototype the user interface by applying utility classes directly in the HTML markup. This approach provided immediate visual feedback and simplified the styling process.
Lessons Learned
Throughout this project, I learned several valuable lessons:
- Contrary to what I believed going into this project, I now feel that I would've been better served beginning with the frontend and building the backend as needed. Going with a backend first approach really created conflicts and limitations on the frontend build.
- Understanding backend and database interactions: Working with Flask, SQLAlchemy, and database migrations enhanced my understanding of how backend systems interact.
- Leveraging frontend frameworks: Utilizing Create React App streamlined the frontend development process, providing a solid foundation for building user interfaces.
- Incorporating Tailwind CSS for rapid prototyping: Tailwind CSS allowed me to prototype the frontend quickly, providing a visual approach to styling directly in the markup. However, it's important to be mindful of potential code clutter and repetitive code that may arise when using utility classes extensively.
- Integrating different parts of the application: Coordinating the frontend and backend requires careful consideration of API endpoints and data structures.
- Learning from existing resources: Researching and studying similar applications online helped me avoid pitfalls and learn best practices.
Conclusion:
Building the "Squatch-Spotter" application from scratch using Flask, React, and Tailwind CSS was an enlightening journey. By carefully configuring the Flask app, defining models, managing migrations, and creating the React frontend with Tailwind CSS, I gained valuable insights into full-stack web development. The combination of Flask, React, and Tailwind CSS provided a powerful toolkit for creating a robust and visually appealing application.
Top comments (0)