In this tutorial, you’ll learn how to build a simple and secure file upload API in Go using the Gin web framework. We’ll support two types of storage: Amazon S3 for cloud storage and local file system for development or offline use. This flexible setup allows you to switch storage backends with minimal changes.
Whether you're building an image uploader, document manager, or any system that handles file uploads, this tutorial will give you a solid foundation.
Prerequisites
-
Go 1.18+
-
Basic knowledge of Go and Gin
-
AWS account (for S3 setup)
-
AWS SDK v2 for Go
-
A REST client like Postman or cURL
Project Setup
1. Initialize the Go Module
mkdir go-upload-api
cd go-upload-api
go mod init github.com/yourusername/go-upload-api
2. Install Dependencies
go get github.com/gin-gonic/gin
go get github.com/aws/aws-sdk-go-v2
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3
3. Directory Structure
Configuration
config/config.go
package config
import "os"
var (
StorageType = os.Getenv("STORAGE_TYPE") // "s3" or "local"
S3Region = os.Getenv("AWS_REGION")
S3Bucket = os.Getenv("AWS_BUCKET")
S3AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
S3SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY")
UploadPath = "./uploads" // for local storage
)
You can use
.env
files with godotenv if desired.
Gin Server and Routes
main.go
package main
import (
"github.com/gin-gonic/gin"
"github.com/yourusername/go-upload-api/handlers"
)
func main() {
router := gin.Default()
router.POST("/upload", handlers.UploadHandler)
router.Run(":8080")
}
Upload Handler
handlers/upload.go
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yourusername/go-upload-api/storage"
)
func UploadHandler(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "File is required"})
return
}
src, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open file"})
return
}
defer src.Close()
url, err := storage.UploadFile(file.Filename, src)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"url": url})
}
Storage Layer
storage/interface.go
(optional for abstraction)
package storage
import (
"io"
)
var UploadFile func(string, io.Reader) (string, error)
storage/local.go
package storage
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/yourusername/go-upload-api/config"
)
func init() {
if config.StorageType == "local" {
UploadFile = uploadLocal
}
}
func uploadLocal(filename string, file io.Reader) (string, error) {
path := filepath.Join(config.UploadPath, filename)
os.MkdirAll(config.UploadPath, os.ModePerm)
out, err := os.Create(path)
if err != nil {
return "", err
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
return "", err
}
return fmt.Sprintf("/uploads/%s", filename), nil
}
storage/s3.go
package storage
import (
"context"
"fmt"
"io"
"github.com/aws/aws-sdk-go-v2/aws"
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/yourusername/go-upload-api/config"
)
var s3Client *s3.Client
func init() {
if config.StorageType == "s3" {
cfg, err := awsConfig.LoadDefaultConfig(context.TODO())
if err != nil {
panic(err)
}
s3Client = s3.NewFromConfig(cfg)
UploadFile = uploadS3
}
}
func uploadS3(filename string, file io.Reader) (string, error) {
_, err := s3Client.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(config.S3Bucket),
Key: aws.String(filename),
Body: file,
ACL: "public-read",
})
if err != nil {
return "", err
}
return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", config.S3Bucket, config.S3Region, filename), nil
}
Run the Go File Upload API
Once you've completed the setup (main.go
, handlers, storage, and config), you can run the app like this:
1. Set Environment Variables
For Local Storage (e.g., during development):
export STORAGE_TYPE=local
For S3 Storage (example):
export STORAGE_TYPE=s3
export AWS_REGION=us-east-1
export AWS_BUCKET=your-bucket-name
export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key
You can also put these in a .env
file and use github.com/joho/godotenv
to load them.
2. Install godotenv
go get github.com/joho/godotenv
3. Create a .env
File in Your Root Directory
# .env
STORAGE_TYPE=s3
AWS_REGION=us-east-1
AWS_BUCKET=your-s3-bucket-name
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
UPLOAD_PATH=./uploads
4. Load .env
in main.go
Modify main.go
to load the .env
file before using os.Getenv
.
package main
import (
"log"
"github.com/joho/godotenv"
"github.com/gin-gonic/gin"
"github.com/yourusername/go-upload-api/handlers"
)
func main() {
// Load .env file
if err := godotenv.Load(); err != nil {
log.Println("No .env file found")
}
router := gin.Default()
router.POST("/upload", handlers.UploadHandler)
router.Run(":8080")
}
Now, all your environment variables like STORAGE_TYPE
, AWS_BUCKET
, etc., will be loaded automatically from .env
.
5. Run the Server
Just run your app normally:
go run main.go
If it .env
exists, it will override your shell environment variables. By default, this starts the server at:
http://localhost:8080
Conclusion
In this tutorial, you learned how to:
-
Set up a file upload API using Go and Gin
-
Upload files to Amazon S3 or local storage
-
Structure a modular and maintainable Go project
This flexible approach enables easy expansion into file validation, database-based metadata storage, or integration with a frontend UI.
You can get the full source code from our GitHub.
That is just the basics. If you need more deep learning about the Golang (Go) language and frameworks, you can take the following cheap course:
- Collaboration and Crawling W/ Golang - Google's Go Language
- How to Build a Golang Blog Engine Web Application
- Fundamentals of Go Learn GoLang Programming Language
- How to Build 1 Million Requests per Minute with Golang
- The Complete React & Golang Course
Thanks!