Image-in that!
A trouble shooting guide for uploading images with React and Flask.
Introduction
If you're like me, as you progress down the path of a software engineer, you often find yourself stuck, banging your head into the metaphorical wall that your code has become. So, you turn to the internet, blog posts, documentation, stackOverflow or any other resource you can think of trying to figure out just what the heck is wrong with your code. Recently, as I worked on my capstone project at Flatiron School, I found myself running into just such an issue while trying to set up file uploading and fetching (specifically with images). While working my way through learning about this topic, I found a wealth of blog posts, articles, videos and other resources about this topic - some of my favorites will be linked at the bottom - however I still, for the life of me could not get it to work.
So, for this blog post, I decided to do things a little differently, I am going to be discussing the setup in my code, both front end and back end and the mistakes I made along the way/the stumbling blocks I encountered and how I fixed those mistakes. To not just present a bland and generic guide to "uploading an image using flask and React" but a guide you can reference to help get you out of those research holes bug fixing can so easily suck you into as well as my improvements and takeaways as my knowledge on the subject grew. Please also note that this is only for static uploads, if you want to upload to the cloud, see the links section at the bottom
The Setup - Getting Started
For my project, I used a file structure template that looked like the following:
Here, my client folder is where I coded all of the React code in the front end, and the server folder is where I put all of the code for my backend. There is an app.py where I wrote the majority of my backend API, migrations(I'll talk more about those later) UPLOADS_FOLDER (this is where we are going to send the images), config.py where I wrote all the configurations and environment variables for my application, models.py where I wrote, you guessed it my models, and a seed.py file for seeing those tables with some data.
The Setup Continues - Backend
Now that the file structure for the application is established, it is time to talk about the backend.
Models
Starting with the models, the first thing it is important to note is that if you are going to be creating something with more than one image, you should create a table specifically for those images as compared to something with only one image (I.E an item posting with multiple item pictures versus a users profile photo). This is one of the parts that at first tripped me up, remember that assigning multiple values to one column of a database table is not only not best practice but can also lead to some bugs, also note that when referencing ItemImages in the front end, they are coming to you as a list of file paths.
class ItemImage(db.Model, SerializerMixin):
__tablename__ = 'item_images'
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey('items.id'))
image_path = db.Column(db.String, nullable=False)
item = db.relationship('Item', back_populates='images')
class Item(db.Model, SerializerMixin):
__tablename__ = 'items'
id = db.Column(db.INTEGER, primary_key=True)
seller_id = db.Column(db.Integer, db.ForeignKey('sellers.id'))
name = db.Column(db.String, nullable=False)
description = db.Column(db.Text, nullable=False)
price = db.Column(db.Float, nullable=False)
images = db.relationship("ItemImage", back_populates="item", cascade="all, delete-orphan")
class User(db.Model, SerializerMixin):
__tablename__ = 'users'
id = db.Column(db.INTEGER, primary_key=True)
username = db.Column(db.VARCHAR(20), unique=True, nullable=False)
email = db.Column(db.String, unique=True, nullable=False)
_password_hash = db.Column(db.String)
profile_photo = db.Column(db.VARCHAR) #This is the FILE PATH to the specific photo
email_notifications = db.Column(db.Boolean, default=False)
As you can see here, a user has a single profile photo, while an item can have multiple images assigned to it relationally. Additionally, it is important to note that the actual data going in the "image column" whether it be for an image or a profile photo is not the data for the file itself, but rather the path that points to where the file is stored.
Config File
The next place in the backend we are going to visit is the config.py file. This is where I establish the various configuration components of my application and environment such as pointing to any secret keys or in this case setting up more code to get file uploads working.
UPLOAD_FOLDER = os.path.join(app.root_path, 'UPLOAD_FOLDER')
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def save_file(file):
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
unique_filename = f"{uuid.uuid4().hex}_{int(time.time())}_{filename}"
file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
file.save(file_path)
return unique_filename
return None
In this section of code, several things are happening, the first two lines are what points to the static uploads folder created in the server folder of the repository.
This is something that took me a while to understand properly, os.path.join
is the pathing to the file/folder on your machine, and the app.route_path comes from Flask and is as the name would suggest, the root path to your flask application. Together these two things are what allow for the files to be sent back and forth from a user to the static uploads to the front end and vice versa.
The other part of this code to pay close attention to is the save_file function, several things are happening here that are vital to the functionality of our application. First is the third line of the function, this creates a unique filename for every file that gets uploaded, this is needed for several reasons. Firstly, it allows the same image to be uploaded multiple times for different users without causing any issues (Trying to reference files with the same name will not work). Finally, notice that while we assign a file path to each file, we are returning the unique_filename
and not the file_path, this is because doing it that way will return the entire path including the pathing from your machine which will cause an issue trying to fetch the image.
API - app.py
Next, we need to cover our backend API and how we handle the images in the actual API. The first route you see utilizes a function imported from Flask, send_from_directory
this function is very similar and even utilizes another Flask function send_file
what it is doing is, pointing to the place in our code where we store files (UPLOADS_FOLDER in our case), looking for a file that matches the filename given and then returning it or sending it up to our front end. Please also note that you must import the allowed_filename and save_file functions from your config file along with the rest of your imports.
@app.route('/uploads/<path:filename>', methods=['GET'])
def serve_uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], secure_filename(filename))
@app.route('/users/<int:user_id>/profile-photo', methods=['PATCH','DELETE'])
def handle_user_profile_photo(user_id):
if request.method == 'PATCH':
return upload_user_profile_photo(user_id)
elif request.method == 'DELETE':
return delete_user_profile_photo(user_id)
else:
return jsonify({'message': 'Method not allowed'}), 405
def upload_user_profile_photo(user_id):
data = request.form
user = db.session.query(User).get(data.get('userId'))
if not user:
return {'message': 'User not found'}, 404
file = request.files.get('profilePhoto')
if not file:
return {'message': 'No file uploaded'}, 400
file_path = save_file(file)
user.profile_photo = file_path
db.session.commit()
return jsonify({'message': 'Profile Photo successfully'}), 204
def delete_user_profile_photo(user_id):
user = db.session.query(User).get(user_id)
if not user:
return {'message': 'User not found'}, 404
if not user.profile_photo:
return jsonify({'message': 'Profile photo not found'}), 404
file_path = user.profile_photo
if os.path.exists(file_path):
os.remove(file_path)
user.profile_photo = None
db.session.commit()
return jsonify({'message': 'Profile Photo successfully'}), 204
Finally included here we have the patch and delete methods for posting a user's profile photo. There are several things to pay attention to here, the first is that the methods are patch and delete, in my front-end code I have established a place for uploading a profile picture separate from where a user creates/edits their account. Next notice for the patch request that the data is not in request.json format, but rather request.form, this is because to upload an image we need to use a form component, and the response will be in form format as well. Additionally, there isrequest.files
which is due to the request containing... dun dun files. We save the profile photo to the images file path and our patch is complete. For the delete we use os.path.exists
to check if whatever image path we have been given is a valid file path and the file exists, then os.remove
is used to delete it from the UPLOADS_FOLDER. Now that an image can be saved, sent back, deleted and any other operations we might need (at least for now) it's time to move to the front.
Moving on- the front end, submitting an Image
In my front end, I used the MUI button component, to streamline the process slightly.
const handleUploadProfilePhoto = async () => {
if (profilePhoto) {
const formData = new FormData();
formData.append('userId', user.id);
formData.append('profilePhoto', profilePhoto);
try {
const response = await fetch(`/users/${user.id}/profile-photo`, {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
},
body: formData,
});
if (response.ok) {
console.log('Profile photo uploaded successfully');
refreshUser(user.id, 'users');
} else {
console.error('Failed to upload profile photo');
}
} catch (error) {
console.error('Upload Profile Photo Error:', error);
}
}
};
<React.Fragment>
<input
accept="image/*"
style={{ display: 'none' }}
id="profile-photo-upload"
type="file"
onChange={handleProfilePhotoChange}/>
<label htmlFor="profile-photo-upload">
<IconButton
size="small"
color="primary"
aria-label="upload profile photo"
component="span"
startIcon={<AddPhotoAlternate />}/>
Click Here to Select Profile Photo
</IconButton>
</label>
{profilePhoto && <Typography variant="body2">{profilePhoto.name}</Typography>}
<Button variant="contained" color="primary" onClick={handleUploadProfilePhoto}>
Submit Photo
</Button>
</React.Fragment>
This code is a snippet from the project that spawned this blog post, and there are quite a few things happening here so let's break it down. First is the handleUploadProfilePhoto function, notice that the request is sent as formData instead of as JSON data, additionally notice that I append the data to the response and assign it the correct value so that the correct user is updated with their uploaded file. The refreshUser function is imported from the application's context and simply causes a re-render with any updated account information.
Next, notice the accept="image/*"
andtype=file
these two lines are what (in conjunction with the <React.Fragment>
and <input>
tags allow for the image to be selected and uploaded. Finally, we have a button that handles the submission of a profile photo on click.
Front End - Displaying your image(s)
Now that we have created code to: save/send images to/from our backend and how to create the front-end component for uploading an image, there is only one last step to view that image on the screen, and that is simply constructing a component using an image tag, making sure we are getting the image from the right place in our backend and finally assign the image.
<Avatar src={`/uploads/${user.profile_photo}`} alt="Profile Photo">
{!user.profile_photo && <Person />}
</Avatar>
Here I use another MUI component, Avatar. I set the source to the '/uploads' path we created in the backend and the route parameter is the user's profile picture. The biggest stumbling blocks here are twofold, first is making sure that your models are serialized properly to ensure you have access to all the right data to make the proper request to the backend. The second is making sure you are pointing to the correct piece of information, I.E make sure you are pointing to the file path itself and not something that points to the file path (Like itemImages from the backend being a list of image paths). With this, I hope that you have the required steps to begin implementing file image and file uploads in your next React + Flask Project.
Takeaways, Conclusion and Extra Resources:
Takeaways and Conclusion:
After completing this project and this blog post, there are several takeaways I have that I will incorporate into future code, the first is very much a personal takeaway, but I used a very small grouping of images that I had on my computer for the testing of this, this allowed me to know, despite the randomized naming and other parts of my code where image paths diverge, where a specific image was or was not working/showing up properly. The second big takeaway is the need to learn more about data structures and storage. After simply using one folder for all of the uploads, I have realized there are more efficient (if more complicated) ways of managing file naming/storage/conventions that I did not make use of that would have ultimately made my code better.
To wrap everything, up after reading through my guide to uploading images (or files) using React and Flask I hope that you as a reader have not just a more comprehensive understanding of how to add image uploading to your next project, but also how to avoid some of the pitfalls and mistakes that can appear when first implementing this finally I hope that you can use the knowledge here to further build and improve your coding skills.
Links /Other Resources:
Image uploading Blogs and Videos:
Cloud Storage and Data Storage: