🔗 files.link
tutorials

How to Upload Files from Go with net/http (No SDK)

Step-by-step guide to uploading files from Go via the files.link REST API — using stdlib net/http and encoding/json, no third-party deps.

Go + REST is a perfect fit

Go's standard library was built for HTTP. net/http, encoding/json, mime/multipart — every primitive you need to talk to a REST API is in the stdlib. No third-party dependencies, no vendored binaries, single-static-binary friendly.

The AWS SDK for Go (aws-sdk-go-v2) is excellent — but it adds tens of MB to your binary footprint when modular packages are pulled in, and it ships its own auth + retry + middleware stack that you have to learn. For straightforward "upload a file to a CDN" workflows, the stdlib is enough.

The 3-step presigned flow

1. POST file metadata to /v1/files/{folderId} with your API key → response has a presigned PUT URL.

2. PUT file bytes to the presigned URL.

3. POST /v1/files/confirm-upload with the file id → row visible.

Code: minimal Go upload

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"
)

const (
    base     = "https://api.files.link"
    folderID = "your-folder-uuid-here"
)

func upload(path string) (string, error) {
    apiKey := os.Getenv("FILESLINK_KEY")
    info, err := os.Stat(path)
    if err != nil {
        return "", err
    }

    // Step 1: ask for a presigned URL
    metaBody, _ := json.Marshal(map[string]any{
        "filesMetadata": []map[string]any{
            {"name": filepath.Base(path), "size": info.Size()},
        },
    })
    metaReq, _ := http.NewRequest("POST", base+"/v1/files/"+folderID, bytes.NewReader(metaBody))
    metaReq.Header.Set("Authorization", apiKey)
    metaReq.Header.Set("Content-Type", "application/json")
    metaRes, err := http.DefaultClient.Do(metaReq)
    if err != nil {
        return "", err
    }
    defer metaRes.Body.Close()
    var metaResp struct {
        URLs []struct {
            URL   string `json:"url"`
            ID    string `json:"id"`
            S3Key string `json:"s3Key"`
        } `json:"urls"`
    }
    json.NewDecoder(metaRes.Body).Decode(&metaResp)
    entry := metaResp.URLs[0]

    // Step 2: PUT the file bytes
    file, _ := os.Open(path)
    defer file.Close()
    putReq, _ := http.NewRequest("PUT", entry.URL, file)
    putReq.ContentLength = info.Size()
    putRes, err := http.DefaultClient.Do(putReq)
    if err != nil {
        return "", err
    }
    putRes.Body.Close()

    // Step 3: confirm
    confirmBody, _ := json.Marshal(map[string]any{"ids": []string{entry.ID}})
    confirmReq, _ := http.NewRequest("POST", base+"/v1/files/confirm-upload", bytes.NewReader(confirmBody))
    confirmReq.Header.Set("Authorization", apiKey)
    confirmReq.Header.Set("Content-Type", "application/json")
    confirmRes, err := http.DefaultClient.Do(confirmReq)
    if err != nil {
        return "", err
    }
    confirmRes.Body.Close()

    return entry.S3Key, nil  // combine with your CDN base URL for a public link
}

func main() {
    key, err := upload("./example.pdf")
    if err != nil {
        fmt.Println("upload failed:", err)
        os.Exit(1)
    }
    fmt.Println("uploaded:", key)
}

Set FILESLINK_KEY in your env, replace folderID, build, run. ~50 lines including imports, zero external deps.

Production considerations

Add a custom *http.Client with timeouts instead of http.DefaultClient — DefaultClient has no timeout, which is a footgun if the network hangs.

client := &http.Client{ Timeout: 30 * time.Second }

Wrap the requests in retry logic (e.g. with github.com/cenkalti/backoff) for 5xx responses. The presigned PUT step is the most likely to fail mid-transfer; retry-safe because PUT is idempotent.

For files larger than ~100MB, use the multipart endpoint. The Go stdlib's sync.WaitGroup makes parallel part uploads trivial.

For more framework-specific Go patterns, see Go file upload.