My Flatiron Magnum Opus: a Journey
Well, the time has come. Phase 5, my final project at Flatiron Bootcamp. A fullstack project making use of everything I’ve learned of frontend and backend since embarking on this journey eight months ago. JavaScript, React, HTML, CSS, Python, Flask, SQL… the mind boggles! Let’s just say there was a lot of Googling involved…
1. Planning
To start with, I generated some project ideas via the notorious Mr. ChatGPT. While none of the ideas particularly appealed to me, they did grease the mechanisms of my own flesh-and-blood thought process. I ultimately landed on something that related to writing, one of my personal passions: a forum to host writing prompts for users to contribute posts to and show appreciation for/discuss the posts of others.
I’ve discovered that, at least as far as software development goes, I’m not much of a planner; I simply lay one brick at a time, starting with the things I know I’ll need. I knew that I’d first need a whole host of related database tables: prompts, users, users’ favorites, and the posts themselves, but with an idea solidified — which I’d always found the most elusive aspect of building a project — the path forward was fairly clear. Or so I thought.
2. The Build
Headache #1:
I’ve lost count of the number of times I’ve been greeted with these gems:
Sworn enemy no.1 showed its unsavory face repeatedly around the beginning of development, when I was finalizing the database models and playing the juggling clown with serialization rules — defining, deleting, modifying, clearing all and restarting from scratch etc. The more related the models are to each other, the more complicated it all gets!
Ultimately, I discovered the most efficient way to pin down the cause — by commenting out all relationships between models and then un-commenting one at a time in a process of elimination. That debugging strategy served me well for other issues, too.
#models.py
#Final working models(!!)
from config import db, bcrypt
favorite = db.Table('favorite',
db.Column('user_id', db.Integer, db.ForeignKey('users.id')),
db.Column('post_id', db.Integer, db.ForeignKey('posts.id'))
)
class User(db.Model, SerializerMixin):
__tablename__ = 'users'
serialize_rules = ('-_password', '-prompts.user','-prompts.posts', '-posts.user', '-favorite_posts.user',)
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
_password = db.Column(db.String(), nullable=False)
created = db.Column(db.DateTime, default=db.func.now())
admin = db.Column(db.Boolean, nullable=False, default=False)
prompts = db.relationship('Prompt', backref='user')
posts = db.relationship('Post', backref='user')
@hybrid_property
def password(self):
raise Exception('Cannot view password hashes')
@password.setter
def password(self, p):
password = bcrypt.generate_password_hash(
p.encode('utf-8')
)
self._password = password.decode('utf-8')
def authenticate(self, p):
return bcrypt.check_password_hash(
self._password, p.encode('utf-8')
)
class Post(db.Model, SerializerMixin):
__tablename__ = 'posts'
serialize_rules = ('-favorited_by_users', '-user.posts', '-user.prompts', '-user.favorite_posts', '-prompt')
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
prompt_id = db.Column(db.Integer, db.ForeignKey('prompts.id'))
created = db.Column(db.DateTime, default=db.func.current_timestamp())
updated_at = db.Column(db.DateTime, onupdate=db.func.now())
favorited_by_users = db.relationship('User', secondary=favorite, backref='favorite_posts')
class Prompt(db.Model, SerializerMixin):
__tablename__ = 'prompts'
serialize_rules = ('-posts.prompt', '-user.prompts', '-user.posts', '-user.favorite_posts')
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String(150), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
created = db.Column(db.DateTime, default=db.func.current_timestamp())
posts = db.relationship('Post', backref='prompt')
Sworn enemy no. 2 was a little less hateful; it invariably meant that the backend was not sending the expected JSON to the frontend, typically because the data was not included in serialization due to the extensive rules outlined above. It was then only a matter of tweaking either the fetch request or the way the data was used in the relevant React component.
Headache #2:
This one was a much stupider waste of time than the first, in that the problem was caused by a dumb oversight, the fix was perfectly simple, and the onus was entirely on me to spot it.
Hot tip: try not to name a database model and an API resource class the same thing. Poor Python will pull from the wrong class when fetching data, then cry when there’s nothing to fetch!
#app.py
class User(Resource):
pass
#Yes, I named it the exact same thing as my User model in models.py...
I only managed to spot this snafu after combing through my past commits on GitHub to understand why my previously working app was now broken. Commenting out my models also helped me realize that only the User model (and not the others) was misbehaving.
3. Deployment
Uuuuuuuuuuugggggggggggggggh…..
Anyone who’s taken a look at the project repo on GitHub can see the number of deployment commits I made (nine. it was nine). Trapped in a seemingly endless cycle of deploy, build failed, modify repo, commit and push, then deploy again was certainly a valuable (and ultimately rewarding) experience, though no less aggravating while stuck in the middle of it.
The key to a successful deployment on Render was this bit of code in app.py:
#app.py
@app.route('/')
@app.route('/<int:id>')
def index(id=0):
return render_template("index.html")
And this configuration of the Flask app, which references a minified version of the React frontend created via ‘npm run build’:
#config.py
app = Flask(
__name__,
static_url_path='',
static_folder='../client/build',
template_folder='../client/build'
)
Database migrations also had to be pushed to the GitHub repo that was connected to Render so that the already-deployed PostgreSQL database could create the proper models.
And, of course, one shouldn’t forget to set the DATABASE_URI variable in ‘app.config’ to the provided PostgreSQL URI on Render! After that, commands like ‘flask db upgrade’ or ‘python seed.py’ (to seed the database) via a local terminal would therefore be sent to the online database instead of a local .db file.
Live site here!
4. Lessons learned
Lesson #1:
Always make sure the app still works before committing. Far preferable to picking up the project again later just to discover that it already needs fixing, as I sadly experienced on multiple occasions. And on a related note, always commit when a new feature has been built and the app has been confirmed to still work!
Lesson #2:
Take breaks. I, personally, would have easily spent all my waking hours in an unbroken stream of coding had I not forced myself to segment my work so that I had the willingness and peace of mind to step away after passing a checkpoint. Build a feature, confirm it works, then take a break to do something else — that strategy was what prevented me from turning into a coding zombie.
5. Looking to the future…
While I have no idea what awaits me following this eight-month journey at Flatiron, I know that I’ll take these hard-won lessons and experiences with me. I also know that despite the frequent headaches and speedbumps that inevitably accompany this kind of work, coding is still what I want to do for a living, even more than before!