Robust media upload with Golang and Cloudinary - Echo Version
Demola Malomo
Posted on February 8, 2022
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 the Echo framework and Cloudinary. At the end of this tutorial, we will learn how to structure an Echo application, integrate Cloudinary with Golang and upload media files to Cloudinary using remote URLs and local file storage.
Echo is a Golang-based HTTP web framework with high performance and extensibility. It supports optimized routing, middlewares, templating, data binding, and rendering.
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.
Prerequisites
The following steps in this post require Golang's experience. Experience with Cloudinary isn’t a requirement, but it’s nice to have.
We will also be needing the following:
- A Cloudinary account to store the media files. Signup is completely free.
- Postman 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 echo-cloudinary-api && cd echo-cloudinary-api
This command creates an echo-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 echo-cloudinary-api
This command will create a go.mod
file for tracking project dependencies.
We proceed to install the required dependencies with:
go get github.com/labstack/echo/v4 github.com/cloudinary/cloudinary-go github.com/joho/godotenv github.com/go-playground/validator/v10
github.com/labstack/echo/v4
is a framework for building web applications.
github.com/cloudinary/cloudinary-go
is a library for integrating Cloudinary.
github.com/joho/godotenv
is a library for managing environment variables.
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:
package main
import (
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.JSON(200, &echo.Map{"data": "Hello from Cloudinary"})
})
e.Logger.Fatal(e.Start(":6000"))
}
The snippet above does the following:
- Import the required dependency.
- Initialize an Echo application using the
New
function. - Use the
Get
function to route to/
path and a, handler function that returns a JSON ofHello from Cloudinary
.echo.Map
is a shortcut formap[string]interface{}
, useful for JSON returns. - Use the
Start
function to run the application on port6000
.
Next, we can test our application by starting the development server by running the command below in our terminal.
go run main.go
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.
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) 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 account to get our Cloud Name, API Key, and API Secret.
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.
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 a 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:
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:
package helper
import (
"context"
config "echo-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 aninterface
as a parameter and returns the remote URL or error if there is any. Theinterface
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 theEnvCloudUploadFolder
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:
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
andUrl
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:
package dtos
import (
"github.com/labstack/echo/v4"
)
type MediaDto struct {
StatusCode int `json:"statusCode"`
Message string `json:"message"`
Data *echo.Map `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:
package services
import (
"echo-cloudinary-api/helper"
"echo-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 thegithub.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 themediaUpload
interface. - Create a
NewMediaUpload
constructor function that ties themedia
struct and themediaUpload
interface it implements. - Create the required methods
FileUpload
andRemoteUpload
with amedia
pointer receiver and returns the URL or error if there is any. The required method also validates inputs from the user and uses theImageUploadHelper
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:
package controllers
import (
"echo-cloudinary-api/dtos"
"echo-cloudinary-api/models"
"echo-cloudinary-api/services"
"net/http"
"github.com/labstack/echo/v4"
)
func FileUpload(c echo.Context) error {
//upload
formHeader, err := c.FormFile("file")
if err != nil {
return c.JSON(
http.StatusInternalServerError,
dtos.MediaDto{
StatusCode: http.StatusInternalServerError,
Message: "error",
Data: &echo.Map{"data": "Select a file to upload"},
})
}
//get file from header
formFile, err := formHeader.Open()
if err != nil {
return c.JSON(
http.StatusInternalServerError,
dtos.MediaDto{
StatusCode: http.StatusInternalServerError,
Message: "error",
Data: &echo.Map{"data": err.Error()},
})
}
uploadUrl, err := services.NewMediaUpload().FileUpload(models.File{File: formFile})
if err != nil {
return c.JSON(
http.StatusInternalServerError,
dtos.MediaDto{
StatusCode: http.StatusInternalServerError,
Message: "error",
Data: &echo.Map{"data": err.Error()},
})
}
return c.JSON(
http.StatusOK,
dtos.MediaDto{
StatusCode: http.StatusOK,
Message: "success",
Data: &echo.Map{"data": uploadUrl},
})
}
The snippet above does the following:
- Import the required dependencies.
- Create a
FileUpload
function that returns anerror
. Inside the function, we first used theFormFile
function to retrieve the formHeader that contains theformFile
object. Secondly, we used theOpen
method attached to the formHeader to retrieve the associated file. We returned the appropriate message and status code using theMediaDto
struct we created earlier for both operations. Thirdly, we used theNewMediaUpload
constructor to access theFileUpload
service by passing theformFile
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:
package controllers
import (
//all import goes here
)
func FileUpload(c echo.Context) error {
//fileupload code goes here
}
func RemoteUpload(c echo.Context) error {
var url models.Url
//validate the request body
if err := c.Bind(&url); err != nil {
return c.JSON(
http.StatusBadRequest,
dtos.MediaDto{
StatusCode: http.StatusBadRequest,
Message: "error",
Data: &echo.Map{"data": err.Error()},
})
}
uploadUrl, err := services.NewMediaUpload().RemoteUpload(url)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
dtos.MediaDto{
StatusCode: http.StatusInternalServerError,
Message: "error",
Data: &echo.Map{"data": "Error uploading file"},
})
}
return c.JSON(
http.StatusOK,
dtos.MediaDto{
StatusCode: http.StatusOK,
Message: "success",
Data: &echo.Map{"data": uploadUrl},
})
}
The RemoteUpload
function does the same thing as the FileUpload
function. However, we created url
variable and validate it using the Echo’s Bind
method. We also passed the variable to the RemoteUpload service as an argument and returned the appropriate response.
Complete media_controller.go
package controllers
import (
"echo-cloudinary-api/dtos"
"echo-cloudinary-api/models"
"echo-cloudinary-api/services"
"net/http"
"github.com/labstack/echo/v4"
)
func FileUpload(c echo.Context) error {
//upload
formHeader, err := c.FormFile("file")
if err != nil {
return c.JSON(
http.StatusInternalServerError,
dtos.MediaDto{
StatusCode: http.StatusInternalServerError,
Message: "error",
Data: &echo.Map{"data": "Select a file to upload"},
})
}
//get file from header
formFile, err := formHeader.Open()
if err != nil {
return c.JSON(
http.StatusInternalServerError,
dtos.MediaDto{
StatusCode: http.StatusInternalServerError,
Message: "error",
Data: &echo.Map{"data": err.Error()},
})
}
uploadUrl, err := services.NewMediaUpload().FileUpload(models.File{File: formFile})
if err != nil {
return c.JSON(
http.StatusInternalServerError,
dtos.MediaDto{
StatusCode: http.StatusInternalServerError,
Message: "error",
Data: &echo.Map{"data": err.Error()},
})
}
return c.JSON(
http.StatusOK,
dtos.MediaDto{
StatusCode: http.StatusOK,
Message: "success",
Data: &echo.Map{"data": uploadUrl},
})
}
func RemoteUpload(c echo.Context) error {
var url models.Url
//validate the request body
if err := c.Bind(&url); err != nil {
return c.JSON(
http.StatusBadRequest,
dtos.MediaDto{
StatusCode: http.StatusBadRequest,
Message: "error",
Data: &echo.Map{"data": err.Error()},
})
}
uploadUrl, err := services.NewMediaUpload().RemoteUpload(url)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
dtos.MediaDto{
StatusCode: http.StatusInternalServerError,
Message: "error",
Data: &echo.Map{"data": "Error uploading file"},
})
}
return c.JSON(
http.StatusOK,
dtos.MediaDto{
StatusCode: http.StatusOK,
Message: "success",
Data: &echo.Map{"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:
package main
import (
"echo-cloudinary-api/controllers"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.POST("/file", controllers.FileUpload)
e.POST("/remote", controllers.RemoteUpload)
e.Logger.Fatal(e.Start(":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
After the uploads, we can check the go-cloudinary
folder on Cloudinary to see uploaded media files.
Conclusion
This post discussed how to structure an Echo application, integrate Cloudinary with Golang and upload media files to Cloudinary using remote URLs and local file storage.
You may find these resources helpful:
Posted on February 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.