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.