Robust media upload with Golang and Cloudinary - Gin-gonic Version
Tags: go, cloudinary, fileupload
Published: 2022-02-08 07:04:04

---
title: Robust media upload with Golang and Cloudinary - Gin-gonic Version
published: true
description: This post discussed how to structure a Gin-gonic application, integrate Cloudinary with Golang and upload media files to Cloudinary
tags: #go #cloudinary #fileUpload #golang
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nn0ch0lvf905lmjk7yky.png
---

The demand for mobile and web applications to support file uploads ranging from images and videos to documents like excel, CSV, and PDF has increased tremendously over the years. It is paramount that we have the required knowledge to integrate file upload support into our applications.

This post will discuss adding media upload support to a REST API using Golang’s [Gin-gonic](https://github.com/gin-gonic/gin) framework and [Cloudinary](https://cloudinary.com/). At the end of this tutorial, we will learn how to structure a Gin-gonic application, integrate Cloudinary with Golang and upload media files to Cloudinary using remote URLs and local file storage.

Gin-gonic, popularly known as **Gin**, is an HTTP web framework written in Golang with performance and productivity support. Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), a lightweight, high-performance HTTP request router that navigates through API routes faster than most frameworks out there.

Cloudinary offers a robust visual media platform to upload, store, manage, transform, and deliver images and videos for websites and applications. The platform also offers a vast collection of software development kits (SDKs) for frameworks and libraries. 

You can find the complete source code in this [repository](https://github.com/Mr-Malomz/gin-cloudinary-api).

# Prerequisites

The following steps in this post require Golang experience. Experience with Cloudinary isn’t a requirement, but it’s nice to have.

We will also be needing the following:

- A [Cloudinary account](https://cloudinary.com/) to store the media files. [**Signup**](https://cloudinary.com/users/register/free) **is completely free**.
- [Postman](https://www.postman.com/downloads/) or any API testing application of your choice.

# Let’s code
## Getting Started

To get started, we need to navigate to the desired directory and run the command below in our terminal:


    mkdir gin-cloudinary-api && cd gin-cloudinary-api

This command creates a  `gin-cloudinary-api` folder and navigates into the project directory.

Next, we need to initialize a Go module to manage project dependencies by running the command below:


    go mod init gin-cloudinary-api

This command will create a `go.mod` file for tracking project dependencies.

We proceed to install the required dependencies with:

    go get -u github.com/gin-gonic/gin github.com/cloudinary/cloudinary-go github.com/joho/godotenv github.com/go-playground/validator/v10

`github.com/gin-gonic/gin` is a framework for building web application.

`github.com/cloudinary/cloudinary-go` is a library for integrating Cloudinary.

`github.com/joho/godotenv` is a library for managing environment variable.

`github.com/go-playground/validator/v10` is a library for validating structs and fields.

## Application Entry Point

With the project dependencies installed, we need to create `main.go` file in the root directory and add the snippet below:

```go
package main

import "github.com/gin-gonic/gin"

func main() {
	router := gin.Default()

	router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{"data": "Hello from Cloudinary"})
	})

	router.Run("localhost:6000")
}
```
The snippet above does the following:

- Import the required dependencies.
- Initialize a Gin router using the `Default` configuration. The `Default` function configures Gin router with default middlewares (logger and recovery).
- Use the `Get` function to route to `/` path and a handler function that returns a JSON of `Hello from Cloudinary`.
- Use the `Run` function to attach the `router` to an **http.Server** and starts listening and serving HTTP requests on `localhost:6000`.

Next, we can test our application by starting the development server by running the command below in our terminal.


    go run main.go


![Testing the app](https://paper-attachments.dropbox.com/s_206E4126DDC79F22A5BA4F5532C504E555C0ED4A5B8EF5C71465200583830900_1643476662897_Screenshot+2022-01-29+at+18.00.12.png)

## Modularization in Golang
It is essential to have a good folder structure for our project. Good project structure simplifies how we work with dependencies in our application and makes it easier for us and others to read our codebase.
To do this, we need to create  `configs`, `services`, `controllers`, `helper`, `models`, and `dtos` folder in our project directory.

![Updated project folder structure](https://paper-attachments.dropbox.com/s_206E4126DDC79F22A5BA4F5532C504E555C0ED4A5B8EF5C71465200583830900_1643666582035_Screenshot+2022-01-31+at+22.33.25.png)


**PS**: *The* `go.sum` *file contains all the dependency checksums, and is managed by the* `go` *tools. We don’t have to worry about it.*

`configs` is for modularizing project configuration files

`services` is for modularizing application logic. It helps keep the controller clean.

`controllers` is for modularizing application  incoming requests and returning responses.

`helper` is for modularizing files used for performing computation of another file.

`models` is for modularizing data and database logics.

`dtos` is for modularizing files describing the response we want our API to give. This will become clearer later on. 
[*Data Transfer Object (DTO)*](https://en.wikipedia.org/wiki/Data_transfer_object) *is simply an object that transfers data from one point to another.*

## Setting up Cloudinary
With that done, we need to log in or sign up into our [Cloudinary](https://cloudinary.com/) account to get our **Cloud Name**, **API Key**, and **API Secret**.

![Cloudinary details](https://paper-attachments.dropbox.com/s_206E4126DDC79F22A5BA4F5532C504E555C0ED4A5B8EF5C71465200583830900_1643493328683_Screenshot+2022-01-29+at+22.53.26.png)


Next, we need to create a folder to store our media uploads. To do this, navigate to the **Media Library** tab, click on the **Add Folder** Icon, input `go-cloudinary` as the folder name and **Save**.

![Create folder](https://paper-attachments.dropbox.com/s_206E4126DDC79F22A5BA4F5532C504E555C0ED4A5B8EF5C71465200583830900_1643498468028_Screenshot+2022-01-30+at+00.18.59.png)



![Create folder](https://paper-attachments.dropbox.com/s_206E4126DDC79F22A5BA4F5532C504E555C0ED4A5B8EF5C71465200583830900_1643498468015_Screenshot+2022-01-30+at+00.19.16.png)


**Setup Environment Variable**
Next, we need to include the parameters from our dashboard into an environment variable. To do this, first, we need to create a `.env` file in the root directory, and in this file, add the snippet below:

    CLOUDINARY_CLOUD_NAME=<YOUR CLOUD NAME HERE>
    CLOUDINARY_API_KEY=<YOUR API KEY HERE>
    CLOUDINARY_API_SECRET=<YOUR API SECRET HERE>
    CLOUDINARY_UPLOAD_FOLDER=go-cloudinary

**Load Environment Variable**
With that done, we need to create helper functions to load the environment variables using the `github.com/joho/godotenv` library we installed earlier.  To do this, we need to navigate to the `configs` folder and in this folder, create an `env.go` file and add the snippet below:
```go
    package config
    
    import (
        "log"
        "os"
        "github.com/joho/godotenv"
    )
    
    func EnvCloudName() string {
        err := godotenv.Load()
        if err != nil {
            log.Fatal("Error loading .env file")
        }
        return os.Getenv("CLOUDINARY_CLOUD_NAME")
    }
    
    func EnvCloudAPIKey() string {
        err := godotenv.Load()
        if err != nil {
            log.Fatal("Error loading .env file")
        }
        return os.Getenv("CLOUDINARY_API_KEY")
    }
    
    func EnvCloudAPISecret() string {
        err := godotenv.Load()
        if err != nil {
            log.Fatal("Error loading .env file")
        }
        return os.Getenv("CLOUDINARY_API_SECRET")
    }
    
    func EnvCloudUploadFolder() string {
        err := godotenv.Load()
        if err != nil {
            log.Fatal("Error loading .env file")
        }
        return os.Getenv("CLOUDINARY_UPLOAD_FOLDER")
    }
```
The snippet above does the following:
- Import the required dependencies.
- Create an `EnvCloudName`, `EnvCloudAPIKey`,  `EnvCloudAPISecret`, `EnvCloudUploadFolder` functions that check if the environment variable is correctly loaded and returns the environment variable.

**Cloudinary helper function**
To facilitate both remote and local upload from our application, we need to navigate to the `helper` folder and in this folder, create a `media_helper.go` file and add the snippet below:
```go
    package helper
    
    import (
        "context"
        config "gin-cloudinary-api/configs"
        "time"
        "github.com/cloudinary/cloudinary-go"
        "github.com/cloudinary/cloudinary-go/api/uploader"
    )
    
    func ImageUploadHelper(input interface{}) (string, error) {
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
    
        //create cloudinary instance
        cld, err := cloudinary.NewFromParams(config.EnvCloudName(), config.EnvCloudAPIKey(), config.EnvCloudAPISecret())
        if err != nil {
            return "", err
        }
    
        //upload file
        uploadParam, err := cld.Upload.Upload(ctx, input, uploader.UploadParams{Folder: config.EnvCloudUploadFolder()})
        if err != nil {
            return "", err
        }
        return uploadParam.SecureURL, nil
    }
```
The snippet above does the following:
- Import the required dependencies.
- Create an `ImageUploadHelper` function that first takes an `interface` as a parameter and returns the remote URLs or error if there is any. The `interface` makes our code reusable by accepting both **remote URL** and a **form file**. The function also does the following:
    - Defined a timeout of 10 seconds when connecting to Cloudinary. 
    - Initialize a new Cloudinary instance by passing in the **Cloud Name**, **API Key**, and **API Secret** as parameters and checking for error if there is any.
    - Upload the media using the `Upload` function and specify the folder to store the media using the `EnvCloudUploadFolder` function. Get both the upload result and error if there is any.
    - Returns the media secure URL and `nil` when there is no error.

## Setup Models and Response Type
**Models**
Next, we need a model to represent our application data. To do this, we need to navigate to the `models` folder, and in this folder, create a `media_model.go` file and add the snippet below:
```go
package models

import "mime/multipart"

type File struct {
    File multipart.File `json:"file,omitempty" validate:"required"`
}

type Url struct {
    Url string `json:"url,omitempty" validate:"required"`
}
```
The snippet above does the following:

- Import the required dependency.
- Create a `File` and `Url` struct with the required property for local file upload and remote URL upload.

**Response Type**
Next, we need to create a reusable `struct` to describe our API’s response. To do this, navigate to the `dtos` folder and in this folder, create a `media_dto.go` file and add the snippet below:
```go
package dtos

type MediaDto struct {
    StatusCode int                    `json:"statusCode"`
    Message    string                 `json:"message"`
    Data       map[string]interface{} `json:"data"`
}
```
The snippet above creates a `MediaDto` struct with `StatusCode`, `Message`, and `Data` property to represent the API response type.

## Finally, Creating REST API’s
With that done, we need to create a **service** to host all the media upload application logics. To do this, navigate to the `services` folder and in this folder, create a `media_service.go` file and add the snippet below:
```go
package services

import (
    "gin-cloudinary-api/helper"
    "gin-cloudinary-api/models"
    "github.com/go-playground/validator/v10"
)

var (
    validate = validator.New()
)

type mediaUpload interface {
    FileUpload(file models.File) (string, error)
    RemoteUpload(url models.Url) (string, error)
}

type media struct {}

func NewMediaUpload() mediaUpload {
    return &media{}
}

func (*media) FileUpload(file models.File) (string, error) {
    //validate
    err := validate.Struct(file)
    if err != nil {
        return "", err
    }
    
    //upload
    uploadUrl, err := helper.ImageUploadHelper(file.File)
    if err != nil {
        return "", err
    }
    return uploadUrl, nil
}

func (*media) RemoteUpload(url models.Url) (string, error) {
    //validate
    err := validate.Struct(url)
    if err != nil {
        return "", err
    }
    
    //upload
    uploadUrl, errUrl := helper.ImageUploadHelper(url.Url)
    if errUrl != nil {
        return "", err
    }
    return uploadUrl, nil
}
```
The snippet above does the following:
- Import the required dependencies.
- Create a `validate` variable to validate models using the `github.com/go-playground/validator/v10` library we installed earlier.
- Create a `mediaUpload` interface with methods describing the type of upload we want to do.
- Create a `media` struct that will implement the `mediaUpload` interface.
- Create a `NewMediaUpload` constructor function that ties the `media` struct and the `mediaUpload` interface it implements.
- Create the required methods `FileUpload` and `RemoteUpload` with a `media` pointer receiver and returns the URL or error if there is any. The required method also validates inputs from the user and uses the `ImageUploadHelper` function we created earlier to upload media to Cloudinary.

**File Upload Endpoint**
With the service setup, we can now create a function to upload media from local file storage. To do this, we need to navigate to the `controllers` folder, and in this folder, create a `media_controller.go` file and add the snippet below:
```go
package controllers

import (
    "gin-cloudinary-api/dtos"
    "gin-cloudinary-api/models"
    "gin-cloudinary-api/services"
    "net/http"
    "github.com/gin-gonic/gin"
)

func FileUpload() gin.HandlerFunc {
    return func(c *gin.Context) {
        //upload
        formfile, _, err := c.Request.FormFile("file")
        if err != nil {
            c.JSON(
                http.StatusInternalServerError,
                dtos.MediaDto{
                    StatusCode: http.StatusInternalServerError,
                    Message:    "error",
                    Data:       map[string]interface{}{"data": "Select a file to upload"},
                })
            return
        }

        uploadUrl, err := services.NewMediaUpload().FileUpload(models.File{File: formfile})
        if err != nil {
            c.JSON(
                http.StatusInternalServerError,
                dtos.MediaDto{
                    StatusCode: http.StatusInternalServerError,
                    Message:    "error",
                    Data:       map[string]interface{}{"data": "Error uploading file"},
                })
            return
        }

        c.JSON(
            http.StatusOK,
            dtos.MediaDto{
                StatusCode: http.StatusOK,
                Message:    "success",
                Data:       map[string]interface{}{"data": uploadUrl},
            })
    }
}
```
The snippet above does the following:
- Import the required dependencies.
- Create a `FileUpload` function that returns a Gin-gonic handler. Inside the returned handler, we first used the `Request.FormFile` function to retrieve the `formFile` object from the request body. We returned the appropriate message and status code using the `MediaDto` struct we created earlier. Secondly, we used the `NewMediaUpload` constructor to access the `FileUpload` service by passing the `formFile` as an argument. The service also returns a URL of the uploaded media or an error if there is any. Finally, we returned the correct response if the media upload was successful.

**Remote URL Upload Endpoint**
To upload an image from a remote URL, we need to modify `media_controller.go` as shown below:
```go
package controllers

import (
    //all import goes here
)

func FileUpload() gin.HandlerFunc {
    return func(c *gin.Context) {
        //fileuplod code goes here
    }
}

func RemoteUpload() gin.HandlerFunc {
    return func(c *gin.Context) {
        var url models.Url

        //validate the request body
        if err := c.BindJSON(&url); err != nil {
            c.JSON(
                http.StatusBadRequest,
                dtos.MediaDto{
                    StatusCode: http.StatusBadRequest,
                    Message:    "error",
                    Data:       map[string]interface{}{"data": err.Error()},
                })
            return
        }

        uploadUrl, err := services.NewMediaUpload().RemoteUpload(url)
        if err != nil {
            c.JSON(
                http.StatusInternalServerError,
                dtos.MediaDto{
                    StatusCode: http.StatusInternalServerError,
                    Message:    "error",
                    Data:       map[string]interface{}{"data": "Error uploading file"},
                })
            return
        }

        c.JSON(
            http.StatusOK,
            dtos.MediaDto{
                StatusCode: http.StatusOK,
                Message:    "success",
                Data:       map[string]interface{}{"data": uploadUrl},
            })
    }
}
```
The `RemoteUpload` function does the same thing as the `FileUpload` function. However, we created `url` variable and validate it using the Gin-gonic’s `BindJSON` method. We also passed the variable to the `RemoteUpload` service as an argument and returned the appropriate response.

**Complete media_controller.go**
```go
package controllers

import (
    "gin-cloudinary-api/dtos"
    "gin-cloudinary-api/models"
    "gin-cloudinary-api/services"
    "net/http"
    "github.com/gin-gonic/gin"
)

func FileUpload() gin.HandlerFunc {
    return func(c *gin.Context) {
        //upload
        formFile, _, err := c.Request.FormFile("file")
        if err != nil {
            c.JSON(
                http.StatusInternalServerError,
                dtos.MediaDto{
                    StatusCode: http.StatusInternalServerError,
                    Message:    "error",
                    Data:       map[string]interface{}{"data": "Select a file to upload"},
                })
            return
        }

        uploadUrl, err := services.NewMediaUpload().FileUpload(models.File{File: formFile})
        if err != nil {
            c.JSON(
                http.StatusInternalServerError,
                dtos.MediaDto{
                    StatusCode: http.StatusInternalServerError,
                    Message:    "error",
                    Data:       map[string]interface{}{"data": "Error uploading file"},
                })
            return
        }

        c.JSON(
            http.StatusOK,
            dtos.MediaDto{
                StatusCode: http.StatusOK,
                Message:    "success",
                Data:       map[string]interface{}{"data": uploadUrl},
            })
    }
}

func RemoteUpload() gin.HandlerFunc {
    return func(c *gin.Context) {
        var url models.Url

        //validate the request body
        if err := c.BindJSON(&url); err != nil {
            c.JSON(
                http.StatusBadRequest,
                dtos.MediaDto{
                    StatusCode: http.StatusBadRequest,
                    Message:    "error",
                    Data:       map[string]interface{}{"data": err.Error()},
                })
            return
        }

        uploadUrl, err := services.NewMediaUpload().RemoteUpload(url)
        if err != nil {
            c.JSON(
                http.StatusInternalServerError,
                dtos.MediaDto{
                    StatusCode: http.StatusInternalServerError,
                    Message:    "error",
                    Data:       map[string]interface{}{"data": "Error uploading file"},
                })
            return
        }

        c.JSON(
            http.StatusOK,
            dtos.MediaDto{
                StatusCode: http.StatusOK,
                Message:    "success",
                Data:       map[string]interface{}{"data": uploadUrl},
            })
    }
}
```
**Putting it all together**
With that done, we need to create a route for our endpoints to upload media from local file storage and remote URL. To do this, we need to modify `main.go` with our controller and specify the relative path as shown below:
```go
package main

import (
    "gin-cloudinary-api/controllers" //add this
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    //add
    router.POST("/file", controllers.FileUpload())
    router.POST("/remote", controllers.RemoteUpload())

    router.Run("localhost:6000")
}
```
With that done, we can test our application by starting the development server by running the command below in our terminal.


    go run main.go


![file upload](https://paper-attachments.dropbox.com/s_206E4126DDC79F22A5BA4F5532C504E555C0ED4A5B8EF5C71465200583830900_1643755301799_Screenshot+2022-02-01+at+00.21.04.png)

![remote url upload](https://paper-attachments.dropbox.com/s_206E4126DDC79F22A5BA4F5532C504E555C0ED4A5B8EF5C71465200583830900_1643755301817_Screenshot+2022-02-01+at+00.46.05.png)


After the uploads, we can check the `go-cloudinary` folder on Cloudinary to see uploaded media files.

![Uploaded media on cloudinary](https://paper-attachments.dropbox.com/s_206E4126DDC79F22A5BA4F5532C504E555C0ED4A5B8EF5C71465200583830900_1643755301873_Screenshot+2022-02-01+at+00.48.52.png)

# Conclusion

This post discussed how to structure a Gin-gonic application, integrate Cloudinary with Golang and upload media files to Cloudinary using remote URLs and local file storage.

You may find these resources helpful:

- [Gin-gonic](https://github.com/gin-gonic/gin)
- [Cloudinary Go SDK](https://cloudinary.com/documentation/go_integration)
- [Go Pointer Receiver](https://go.dev/tour/methods/4).