Static Files

Serve static files and directories efficiently.

The router provides methods for serving static files and directories.

Directory Serving

Serve an entire directory.

r := router.MustNew()

// Serve ./public/* at /assets/*
r.Static("/assets", "./public")

// Serve /var/uploads/* at /uploads/*
r.Static("/uploads", "/var/uploads")

Example:

./public/
├── css/
│   └── style.css
├── js/
│   └── app.js
└── images/
    └── logo.png

Access:

  • http://localhost:8080/assets/css/style.css
  • http://localhost:8080/assets/js/app.js
  • http://localhost:8080/assets/images/logo.png

Single File Serving

Serve specific files:

r.StaticFile("/favicon.ico", "./static/favicon.ico")
r.StaticFile("/robots.txt", "./static/robots.txt")

Custom File System

Use a custom filesystem.

import "net/http"

r.StaticFS("/files", http.Dir("./files"))

Embedded Files

Go 1.16 added embed.FS which lets you put files inside your binary. This is great for single-file deployments — no need to copy static files around.

The router has a helper method that makes this easy:

import "embed"

//go:embed web/dist/*
var webAssets embed.FS

r := router.MustNew()

// Serve web/dist/* at /assets/*
r.StaticEmbed("/assets", webAssets, "web/dist")

The third parameter ("web/dist") tells the router which folder inside the embed to use. This strips that prefix from the URLs.

Why use embedded files?

  • One binary — Deploy a single file, no folders to manage
  • Fast startup — Files are already in memory
  • Safe — Nobody can change your static files at runtime

Example project layout:

myapp/
├── main.go
└── web/
    └── dist/
        ├── index.html
        ├── css/
        │   └── style.css
        └── js/
            └── app.js

Serving a frontend app:

package main

import (
    "embed"
    "net/http"
    "rivaas.dev/router"
)

//go:embed web/dist/*
var webAssets embed.FS

func main() {
    r := router.MustNew()
    
    // Serve your frontend at the root
    r.StaticEmbed("/", webAssets, "web/dist")
    
    // API routes
    r.GET("/api/status", func(c *router.Context) {
        c.JSON(200, map[string]string{"status": "OK"})
    })
    
    http.ListenAndServe(":8080", r)
}

Now http://localhost:8080/ serves index.html, and http://localhost:8080/css/style.css serves your CSS.

File Serving in Handlers

Serve File

r.GET("/download/:filename", func(c *router.Context) {
    filename := c.Param("filename")
    filepath := "./uploads/" + filename
    c.ServeFile(filepath)
})

Force Download

r.GET("/download/:filename", func(c *router.Context) {
    filename := c.Param("filename")
    filepath := "./reports/" + filename
    c.Download(filepath, "report-2024.pdf")
})

Wildcard Routes for File Serving

r.GET("/files/*filepath", func(c *router.Context) {
    filepath := c.Param("filepath")
    fullPath := "./public/" + filepath
    c.ServeFile(fullPath)
})

Complete Example

Here’s a full example with all the ways to serve static files:

package main

import (
    "embed"
    "net/http"
    "rivaas.dev/router"
)

//go:embed static/*
var staticAssets embed.FS

func main() {
    r := router.MustNew()
    
    // Option 1: Serve from filesystem
    r.Static("/assets", "./public")
    
    // Option 2: Serve embedded files
    r.StaticEmbed("/static", staticAssets, "static")
    
    // Serve specific files
    r.StaticFile("/favicon.ico", "./static/favicon.ico")
    r.StaticFile("/robots.txt", "./static/robots.txt")
    
    // Custom file serving with download
    r.GET("/downloads/:filename", func(c *router.Context) {
        filename := c.Param("filename")
        c.Download("./files/"+filename, filename)
    })
    
    // API routes
    r.GET("/api/status", func(c *router.Context) {
        c.JSON(200, map[string]string{"status": "OK"})
    })
    
    http.ListenAndServe(":8080", r)
}

Security Considerations

Path Traversal Prevention

// ❌ BAD: Vulnerable to path traversal
r.GET("/files/*filepath", func(c *router.Context) {
    filepath := c.Param("filepath")
    c.ServeFile(filepath) // Can access ../../../etc/passwd
})

// ✅ GOOD: Validate and sanitize paths
r.GET("/files/*filepath", func(c *router.Context) {
    filepath := c.Param("filepath")
    
    // Validate path
    if strings.Contains(filepath, "..") {
        c.Status(400)
        return
    }
    
    // Serve from safe directory
    c.ServeFile("./public/" + filepath)
})

Best Practices

  1. Use absolute paths for static directories
  2. Validate file paths to prevent traversal attacks
  3. Set appropriate cache headers for static assets
  4. Use CDN for production static assets
  5. Serve from dedicated file server for large files
  6. Use embed.FS for single-binary deployments (great for containers and CLI tools)

Next Steps