DEV Community

Cover image for Build a Local Image File Uploader from Scratch in Go
Vinit Jogi
Vinit Jogi

Posted on

Build a Local Image File Uploader from Scratch in Go

🚀Getting Started with File Uploads in Go (for Beginners)

Ever wondered how to build your own image uploader without relying on third-party services or complex libraries? In this tutorial, I’ll walk you through creating a simple, beginner-friendly local image file uploader using Go, HTML, CSS, and a bit of JavaScript. This project is perfect for developers just starting with Go who want to understand how file handling works behind the scenes. By the end, you'll have a fully functional uploader that stores images on your local filesystem — and a solid foundation to build more advanced features on top of it.

🧱 Tech Stack Used

  • Frontend: HTML, CSS and JS, To create a basic form for uploading files.
  • Backend: Go (net/http), to handle file upload and server logic.
  • Storage: Local filesystem, to save uploaded images in a local directory.

📁 Project Structure

Here's how the project is organized:

file-uploader/
├── cmd/
│   └── file-uploader/
│       └── main.go              # Entry point of the application
├── internal/
│   ├── handlers/
│   │   └── upload.go            # Upload handler logic
│   ├── utilities/
│   │   └── utilities.go         # Utility/helper functions
│   └── web/
│       ├── index.html           # Frontend upload form
│       └── main.js              # Frontend JS logic (optional)
├── uploads/                     # Folder to store uploaded images
├── .gitignore                   # Git ignore rules
└── go.mod                       # Go module file
Enter fullscreen mode Exit fullscreen mode

Alright, now that we’ve covered the basics — let’s roll up our sleeves and start building this image uploader step by step! 💪🛠️

Step 1: 🧱Generating a go.mod file
First things first — open your favorite terminal, cd into your project directory, and run the following command to initialize a Go module:

go mod init <your_module_name>
Enter fullscreen mode Exit fullscreen mode

Replace <your_module_name> with the name or path you want for your project (e.g., github.com/yourusername/file-uploader or just go-file-uploader for local projects).

This creates a go.mod file, which helps Go manage dependencies and track your module.

Step 2: 🚀Building a file upload endpoint
Now let’s create a simple Go server that can handle file uploads and serve static files (like your HTML form and JavaScript).

Here is what the main.go file inside cmd/file-uploader/ looks like:

package main

import (
    "fmt"
    "go-file-uploader/internal/handlers"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/upload", handlers.FileUploadHandler)

    // Serve static files (HTML, JS, CSS)
    fs := http.FileServer(http.Dir("internal/web"))
    http.Handle("/", fs)

    fmt.Println("Server running on port: 8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Enter fullscreen mode Exit fullscreen mode

🔍 What’s Happening Here?

  1. We define a /upload endpoint and link it to FileUploadHandler(which we'll define next).
  2. The root path / serves static frontend files from the internal/web directory.
  3. The server runs on localhost:8080.

Step 3: 📤Writing the File Upload Logic

Let’s move on to the actual file upload handling. This logic lives in internal/handlers/upload.go.

package handlers

import (
    "fmt"
    "go-file-uploader/internal/utilities"
    "io"
    "net/http"
    "strings"
)

// Validate the file type; currently only allowing image uploads
func isValidFileType(file []byte) bool {
    fileType := http.DetectContentType(file)
    return strings.HasPrefix(fileType, "image/")
}

func FileUploadHandler(w http.ResponseWriter, r *http.Request) {
    /*
        limiting the file size to 10 mb
        left shift operator. Shift the bits of 10, 20 times to the left
        effectively it is 10 * 2^20.
    */
    r.ParseMultipartForm(10 << 20)

    // Retrieve the uploaded file from the form

    file, handler, err := r.FormFile("myFile")

    if err != nil {
        http.Error(w, "Error retrieving the file", http.StatusBadRequest)
        return
    }

    defer file.Close()

    fileBytes, err := io.ReadAll(file)
    if err != nil {
        http.Error(w, "Invalid file", http.StatusBadRequest)
        return
    }

    if !isValidFileType(fileBytes) {
        http.Error(w, "Invalid file type", http.StatusUnsupportedMediaType)
        return
    }

    // Save the file locally to the uploads directory

    dst, err := utilities.CreateFile(handler.Filename)

    if err != nil {
        http.Error(w, "Error in saving the file", http.StatusInternalServerError)
        return
    }

    defer dst.Close()

    // Write the uploaded file bytes to the destination file

    if _, err := dst.Write(fileBytes); err != nil {
        http.Error(w, "Error saving the file", http.StatusInternalServerError)
    }

    fmt.Fprintf(w, "Uploaded file: %s\n", handler.Filename)
    fmt.Fprintf(w, "File Size: %.2f KB\n", float64(handler.Size)/(1024))
    // fmt.Fprintf(w, "MIME Header: %v\n", handler.Header)
    fmt.Fprintf(w, "File uploaded successfully: %s\n", handler.Filename)
}

Enter fullscreen mode Exit fullscreen mode

🔍 What’s Happening Here?

  1. We limit file size to 10MB using ParseMultipartForm(10 << 20).
  2. We retrieve the uploaded file via r.FormFile("myFile"). Make sure the name "myFile" matches the name attribute of the input field in your HTML form — otherwise, Go won’t be able to read the file correctly.
  3. We read and validate the file type using Go's http.DetectContentType() (only allowing image types).
  4. We save the uploaded file using a utility function (CreateFile), which we’ll define in the next step.
  5. Finally, we print out some details as a response after a successful upload.

This approach is simple yet safe, ensuring only valid image files get saved on your system.

Step 4: 🛠️Creating the CreateFile() Utility Function

Now let’s define a utility function to save uploaded files inside a local uploads/ directory. This function lives in internal/utilities/utilities.go.

package utilities

import (
    "os"
    "path/filepath"
)

func CreateFile(filename string) (*os.File, error) {
    // Create the uploads directory if it doesn't exist
    if _, err := os.Stat("uploads"); os.IsNotExist(err) {
        os.Mkdir("uploads", 0755)
    }

    // Build the full file path and create the file
    dst, err := os.Create(filepath.Join("uploads", filename))
    if err != nil {
        return nil, err
    }

    return dst, nil
}

Enter fullscreen mode Exit fullscreen mode

🔍 What’s Happening Here?

  1. We check whether the uploads/ folder exists using os.Stat().
  2. If it doesn’t exist, we create it with permission 0755 using os.Mkdir().
  3. We then build the full path for the file using filepath.Join() — this ensures proper path formatting across OSes.
  4. Finally, we create the file and return it so the handler can write to it.

This utility abstracts away the file creation logic, keeping your upload handler clean and focused.

With the backend ready to handle image uploads, it’s time to move on to the fun part — building a simple user interface to test our uploader. Let’s jump into the HTML form next! 🧑‍💻📤

Step 5: 🎨Building the Frontend UI

To test our upload logic, we need a simple and clean user interface. The HTML file below creates a styled upload form with a file input, a submit button, and a placeholder to show the upload status.

Save this as index.html in your internal/web/ directory:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Upload file locally using Go-Lang</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f9f9f9;
            padding: 40px;
        }

        .container {
            max-width: 500px;
            margin: auto;
            padding: 30px;
            background-color: #fff;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            border-radius: 8px;
            text-align: center;
        }

        input[type="file"] {
            margin-bottom: 20px;
        }

        button {
            padding: 10px 20px;
            font-size: 16px;
            background-color: #0077cc;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        button:hover {
            background-color: #005fa3;
        }

        #status {
            margin-top: 20px;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Upload a File</h1>
        <form id="uploadForm" enctype="multipart/form-data">
            <input type="file" name="myFile" id="fileInput" required />
            <button type="submit">Upload</button>
        </form>
        <div id="status"></div>
    </div>
    <script src="/main.js"></script>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode

💡 What’s Happening Here?

  1. The form uses enctype="multipart/form-data" — which is essential for file uploads.
  2. The input field has name="myFile" to match the backend’s expected form key.
  3. There's an main.js script (we’ll write next) that can handle form submission using JavaScript.
  4. CSS is included within the <style> block for a nice, clean layout — no external files needed.

Step 6: ⚙️ Adding JavaScript to Handle the Upload

document.getElementById("uploadForm").addEventListener("submit", async function (e) {
    e.preventDefault();

    const fileInput = document.getElementById("fileInput");
    const formData = new FormData();
    formData.append("myFile", fileInput.files[0]);

    const response = await fetch("/upload", {
        method: "POST",
        body: formData,
    });

    const status = document.getElementById("status");
    if (response.ok) {
        const text = await response.text();
        status.innerText = `Success: \n ${text}`;
    } else {
        status.innerText = `Failed to upload file. The file should be of type image and should be less than 10MB.`;
    }
});

Enter fullscreen mode Exit fullscreen mode

🔍 What’s Happening Here?

  1. We attach a submit event listener to the form.
  2. When the form is submitted, we prevent the default page reload using e.preventDefault().
  3. We collect the selected file from the input field and append it to a FormData object.
  4. We then send a POST request to the /upload endpoint using fetch().
  5. Based on the response, we update the page with a success or error message.

This makes your uploader more dynamic and user-friendly — no page reloads needed!

🧪 Final Step: Run and Test Your Uploader

▶️ Run the Go Server
In your terminal, navigate to the root of your project and run:

go run cmd/file-uploader/main.go
Enter fullscreen mode Exit fullscreen mode

You should see:

Server running on port: 8080
Enter fullscreen mode Exit fullscreen mode

🌐 Test in the Browser
Open your browser and go to:

  1. http://localhost:8080.
  2. You should see a clean UI with a file input and upload button.Output-Image

  3. Choose an image file (e.g., .png, .jpg) and click Upload.

  4. If successful, you’ll see something like:

Output-Image

🧾 Final Thoughts

And that’s it! 🎉 You’ve just built a fully functional local image file uploader using Go, HTML, CSS, and JavaScript — without relying on any external libraries or cloud services.

This project is a great starting point for understanding:

  • How to handle file uploads in Go
  • How to validate file types and manage uploads securely
  • How to connect a simple frontend with a backend

From here, you can take it further by:

  • Preventing file overwrites
  • Displaying uploaded images on the page
  • Adding drag-and-drop support
  • Saving file metadata in a database
  • Eventually integrating cloud storage like AWS S3

🧑‍💻 Explore the code here:
👉GitHub Repository

Thanks for following along — and happy coding! 👨‍💻✨

Top comments (0)