Image Uploads in a React/Go Stack — Part 2

James Howard
3 min readMar 31, 2024

--

This article continues on from part 1 and goes through how I added image upload to my Go API. The goal here is to get the endpoints implemented for the image update and for image requests. This will require some file handling and finding somewhere to store the image files.

Getting Started

The first step is to set up routes and the associated controller functions. This is fairly simple but as usual, I need to be sure to implement the equivalent options route to be sure that CORS work. This is done using a Preflight function and a CORS middleware that I’ve documented here.

protected := r.Group("/api/admin")
protected.Use(middlewares.JwtAuthMiddleware())
protected.Use(middlewares.CORSMiddleware())protected.OPTIONS("/user", controllers.Preflight)
protected.OPTIONS("/user/profile_image", controllers.Preflight)
protected.POST("/user/profile_image", controllers.UploadUserProfileImage)

Controller Functionality

In the controller, I need to make sure that my input is reasonably formatted, extract the user ID from the token and generate sensible errors. Aside from that, there isn’t a lot going on.

func UploadUserProfileImage(c *gin.Context) {
formFile, header, err := c.Request.FormFile("imageFile")

if err != nil {
errorText, code := parseProfileImageError(err)
c.JSON(http.StatusBadRequest, gin.H{"message": errorText, "code": code})
return
}
userID, _ := token.GetUserIDFromContextToken(c)

_, err = models.SaveUserProfileFile(userID, formFile)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error(), "code": errors.ImageFileSaveFailed})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Request OK", "data": gin.H{"name": header.Filename, "size": header.Size, "user_id": userID}})
}

You’ll notice there is a call to a function that parses errors if required.

func parseProfileImageError(err error) (string, int) {
errorText := err.Error()
code := errors.ImageFileMissing
if strings.Contains(errorText, "multipart/form-data") {
code = errors.ImageUploadNotMultipart
}
return errorText, code
}

Generally when writing APIs I like to return an unambiguous error code. This habit is to make life easier later if I need to localise the client code into another language since I can then localise the error messages without having to modify the API codecase

Storing the Image

Since I’m working without an object store and CDN, I’ll need to write any images to the local filesystem of the server. This is obviously a short-term solution. I’m also going to try to avoid updating the database and simplify file management so I’ll need to rename the image file to include the user ID in the filename.

So the general approach is to generate a save file name, open a file to save it and then copy the data from the form file into the save file. Before I start this, it’s probably wise to write a bit of a test to make sure that I can access the configured filesystem location. I’ll unit test everything else, but a filesystem check is always particularly helpful to make sure that the environment is properly set up.

func TestProfileFileAccess(t *testing.T) {
err := godotenv.Load("../.env")

path := os.Getenv("PROFILE_IMAGE_STORAGE")
assert.Empty(t, err)
assert.NotEmpty(t, path)
stats, err := os.Stat(path)
assert.False(t, os.IsNotExist(err))
assert.NotEmpty(t, stats)

file, err := os.Create(path + "/testfile.txt")
assert.Empty(t, err)

testString := "This is a test"
writeSize, err := file.WriteString(testString)
assert.Empty(t, err)
assert.Equal(t, len(testString), writeSize)
file.Close()
}

Once the test is sorted out and the file system access is sorted , it remains to get the basic function done and then on to checking it all works.

func SaveUserProfileFile(userID uint, formFile multipart.File) (string, error) {
saveFileName := os.Getenv("PROFILE_IMAGE_STORAGE") +
"/profile_image_" + strconv.Itoa(int(userID))
defer formFile.Close()

saveFile, err := os.OpenFile(saveFileName, os.O_WRONLY|os.O_CREATE,
os.ModePerm)
if err != nil {
return "", err
}
defer saveFile.Close()

_, err = io.Copy(saveFile, formFile)
return saveFileName, err
}

My tool of choice for this is Postman but I’ll probably come back and add swagger to the project in a bit. The setup on this is pretty simple and once it’s setup checking the back-end is a breeze.

Finally, I made sure the various error conditions all show up correctly in the API and the back-end is ready for integration. I’ll cover this and the finishing touches in Part 3.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

James Howard
James Howard

No responses yet

Write a response