How to Organize Assets and Static Files in Go Web Applications

Use the golang-standards/project-layout convention by placing runtime static files in /web/static, HTML templates in /web/template, compiled front-end bundles in /web/app, and repository assets like logos in /assets to maintain clean separation between Go source code and web resources.

Organizing assets and static files in Go web applications requires a deliberate directory structure that keeps web resources accessible to your HTTP server without cluttering your Go source tree. The golang-standards/project-layout repository defines community-standard conventions for positioning these files outside cmd/, pkg/, and internal/ while supporting both filesystem-based development and embedded production binaries. Following this layout ensures your application can switch between http.FileServer serving and Go 1.16+ embed directives without changing directory paths or application logic.

Standard Directory Structure for Web Assets

The project-layout specification establishes four distinct locations for different asset types, each serving a specific purpose in the development and deployment lifecycle.

/web/static for HTTP Server Assets

Place CSS, JavaScript, images, fonts, and other files that your application serves directly to clients into /web/static. This directory functions as the document root for your HTTP handlers and remains logically isolated from your Go packages, making it easy to cache via CDN or reverse proxy. The repository includes a placeholder at /web/static/.keep to signal this directory should exist in version control even when empty.

/web/template for Server-Side Rendering

Store HTML templates and partials used with text/template or html/template in /web/template. This separation allows you to parse template sets using template.ParseFS or template.ParseGlob without traversing unrelated static assets. The placeholder file /web/template/.keep marks this location for version control.

/web/app for Compiled Front-End Bundles

Single-page applications (SPAs) and other compiled JavaScript artifacts belong in /web/app. This directory holds production builds from React, Vue, or Angular projects that your Go backend serves as static content, distinguishing compiled output from raw source files in /web/static.

/assets for Repository Resources

Use the top-level /assets directory for files that support the repository itself but never serve over HTTP, such as README screenshots, documentation diagrams, and CI badges. As documented in /assets/README.md, these resources exist for presentation and build tooling rather than runtime web serving.

Development and Production Serving Strategies

Go web applications support two primary patterns for delivering assets. The project-layout structure accommodates both without requiring directory reorganization.

Serving from the Filesystem During Development

For local development, serve assets directly from disk to enable rapid iteration without recompiling your binary. Use http.FileServer combined with http.StripPrefix to map URL paths to your web/static directory:

package main

import (
	"net/http"
)

func main() {
	// Serve everything under /web/static at the URL path /static/
	fs := http.FileServer(http.Dir("./web/static"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))

	http.ListenAndServe(":8080", nil)
}

With this configuration, a file located at web/static/css/style.css becomes accessible at http://localhost:8080/static/css/style.css.

Embedding Assets for Single-Binary Deployment

With Go 1.16+, embed the entire web/static and web/template trees directly into your executable using the //go:embed directive. This approach produces a single distributable file while preserving the logical directory structure:

package main

import (
	"embed"
	"io/fs"
	"net/http"
)

//go:embed web/static/*
//go:embed web/template/*
var embeddedFS embed.FS

func main() {
	// Create an http.FileSystem from the embedded static files
	sub, _ := fs.Sub(embeddedFS, "web/static")
	staticFS := http.FileServer(http.FS(sub))

	http.Handle("/static/", http.StripPrefix("/static/", staticFS))

	// Load templates from the embedded filesystem
	// tmpl, _ := template.ParseFS(embeddedFS, "web/template/*.html")

	http.ListenAndServe(":8080", nil)
}

The fs.Sub function isolates the web/static subtree so URL paths remain consistent between development and embedded modes. Templates parse directly from embeddedFS using template.ParseFS without extracting files to disk.

Accessing Repository Assets at Build Time

Assets stored in /assets require standard file system operations since they remain outside the embedded web tree. Access these during build processes or documentation generation:

// Example: reading a logo for inclusion in a generated PDF
data, err := os.ReadFile("assets/logo.png")
if err != nil {
    log.Fatal(err)
}
_ = data // use the binary data as needed

Key Implementation Files

The project-layout repository includes specific documentation and placeholders that establish this structure:

  • /assets/README.md – Explains the distinction between repository assets and runtime web files.
  • /web/static/.keep – Preserves the static directory in version control.
  • /web/template/.keep – Placeholder for server-side template storage.
  • /web/app/.keep – Indicates the location for SPA build artifacts.
  • /web/README.md – Provides overview documentation for the web resource hierarchy.

Summary

  • Place HTTP-servable static files in /web/static and keep server-side templates in /web/template to maintain separation from Go source code.
  • Store repository-specific assets like documentation images in /assets, intentionally excluding them from runtime web serving.
  • Use /web/app for compiled front-end bundles such as React or Vue SPA artifacts.
  • Serve files directly from disk during development using http.FileServer and http.StripPrefix for rapid iteration.
  • Embed assets into your binary using //go:embed directives and embed.FS for single-executable deployments in production.
  • Reference the /web/README.md and placeholder files in the golang-standards/project-layout repository to ensure your structure aligns with Go community standards.

Frequently Asked Questions

What is the difference between /assets and /web/static?

The /assets directory contains repository-level files like README screenshots and documentation images that support the project presentation but never serve over HTTP. In contrast, /web/static holds runtime assets such as CSS, JavaScript, and images that your Go application serves directly to clients via http.FileServer or embeds into the binary.

How do I serve embedded static files with the correct URL prefix?

Use fs.Sub to extract the specific subdirectory from your embed.FS before passing it to http.FileServer. For example, fs.Sub(embeddedFS, "web/static") isolates the static content, allowing you to strip the /static/ prefix with http.StripPrefix so that web/static/css/style.css maps correctly to /static/css/style.css in the browser.

Can I use this layout with a React or Vue single-page application?

Yes, place your compiled SPA bundle files in /web/app as designated by the project-layout standard. Configure your Go server to serve the index.html and static artifacts from this directory, or move the built assets into /web/static if you prefer serving them alongside other static files using the same http.FileServer handler.

Where should HTML templates be stored in a Go web project?

Store HTML templates and partials in /web/template, a dedicated location separate from static assets. This allows you to parse templates efficiently using template.ParseFS when embedding or template.ParseGlob when serving from disk, maintaining clean organization between your presentation logic and style resources.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →