Documentation

NexGo Reference

Complete documentation for the NexGo web framework β€” the Go-powered alternative to Next.js.

v2.2.1 Go 1.22+ Zero Dependencies

Quick Start

Get a NexGo project running in under 2 minutes.

1. Install NexGo

$go install github.com/salmanfaris22/nexgo/v2/cmd/nexgo@v2.2.1

⚠️ PATH Setup (Important):

export PATH=$PATH:$(go env GOPATH)/bin
echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.bashrc && source ~/.bashrc

2. Create a New Project

nexgo create my-app
cd my-app
go mod tidy
nexgo dev
Terminal Output
βœ“ Created project my-app with NexGo template
  β†’ pages/index.html
  β†’ layouts/default.html
  β†’ static/css/global.css
  β†’ nexgo.config.json
  β†’ main.go

βœ“ Dependencies installed

[NexGo] Dev server β†’ http://localhost:3000
[NexGo] Hot reload enabled. Watching 5 files…
[NexGo] Press Ctrl+C to stop.

Open http://localhost:3000 β€” your app is live with hot reload enabled.

Project Structure

Running nexgo create my-app generates this structure:

my-app/
β”œβ”€β”€ main.go                  # Application entry point
β”œβ”€β”€ nexgo.config.json        # Framework configuration
β”œβ”€β”€ go.mod
β”‚
β”œβ”€β”€ pages/                   # File-based routes
β”‚   β”œβ”€β”€ index.html           #   β†’ /
β”‚   β”œβ”€β”€ about.html           #   β†’ /about
β”‚   β”œβ”€β”€ blog/
β”‚   β”‚   β”œβ”€β”€ index.html       #   β†’ /blog
β”‚   β”‚   └── [slug].html      #   β†’ /blog/:slug (dynamic)
β”‚   └── api/
β”‚       └── hello.go         #   β†’ /api/hello
β”‚
β”œβ”€β”€ layouts/
β”‚   └── default.html         # Default layout
β”œβ”€β”€ components/              # Reusable partials
β”œβ”€β”€ static/                  # Static assets β†’ /static/
β”‚   β”œβ”€β”€ css/global.css
β”‚   └── js/
└── .nexgo/out/              # Build output (generated)

File-Based Routing

Drop HTML files in pages/ and NexGo creates routes automatically β€” no manual registration needed.

File Path URL Pattern Example
pages/index.html//
pages/about.html/about/about
pages/blog/index.html/blog/blog
pages/blog/[slug].html/blog/:slug/blog/my-post
pages/docs/[...path].html/docs/*/docs/api/auth
pages/api/users.go/api/users/api/users

Dynamic Routes

Use [param] syntax for dynamic URL segments:

pages/
β”œβ”€β”€ users/
β”‚   β”œβ”€β”€ [id].html          β†’ /users/123
β”‚   └── [id]/posts.html    β†’ /users/123/posts
└── [...catchall].html     β†’ /any/path/here

Access parameters in templates:

<h1>User {"{"}{ .Params.id }{"}"}</h1>
<p>Post: {"{"}{ .Params.slug }{"}"}</p>
Rendered Output β€” /users/42
localhost:3000/users/42

User 42

Post: my-first-blog

Layouts & Components

Layouts wrap page content. NexGo walks up the directory tree to find the nearest layout:

<!-- layouts/default.html -->
<!DOCTYPE html>
<html>
<head>
  <title>{"{"}{ .Title }{"}"}</title>
  <link rel="stylesheet" href="/static/css/global.css">
</head>
<body>
  <nav>...</nav>
  
  <footer>...</footer>
</body>
</html>

Reusable template partials stored in components/:

<!-- components/card.html -->
<div class="card">

</div>

<!-- Usage in pages -->

Template Engine

NexGo uses Go's html/template package. Every page receives a PageData struct:

type PageData struct {
    Title        string
    Description  string
    Path         string                 // Current URL path
    Params       map[string]string      // Route params
    Query        map[string]string      // Query string
    Props        map[string]interface{} // From DataLoader
    DevMode      bool
    BuildID      string
}

API Routes

Create .go files in pages/api/ to build REST endpoints:

// pages/api/hello.go
package api

import (
    "net/http"
    "github.com/salmanfaris22/nexgo/pkg/api"
    "github.com/salmanfaris22/nexgo/pkg/router"
)

func init() {
    router.RegisterAPI("/api/hello", Hello)
}

func Hello(w http.ResponseWriter, r *http.Request) {
    api.JSON(w, map[string]interface{}{
        "message": "Hello from NexGo!",
        "method":  r.Method,
    })
}
API Response β€” GET /api/hello
HTTP/1.1 200 OK
Content-Type: application/json

{
  "message": "Hello from NexGo!",
  "method":  "GET"
}

Data Loaders

Data loaders work like Next.js getServerSideProps β€” fetch data server-side before rendering:

// main.go
srv.RegisterDataLoader("/blog/[slug]", func(
    req *http.Request,
    params map[string]string,
) (map[string]interface{}, error) {
    slug := params["slug"]
    post, err := db.GetPost(slug)
    if err != nil {
        return nil, err
    }
    return map[string]interface{}{"post": post}, nil
})
<!-- pages/blog/[slug].html -->
<h1>{"{"}{ .Props.post.Title }{"}"}</h1>
Rendered Output β€” /blog/hello-world
localhost:3000/blog/hello-world

Getting Started with NexGo

Data loaded server-side via DataLoader before page render

Middleware

Built-in middleware is applied in this order:

Request β†’ Recover β†’ Logger β†’ SecurityHeaders β†’ [Gzip] β†’ Handler
Server Log β€” Middleware in action
[NexGo] GET  /about       200  1.2ms
[NexGo] POST /api/users   201  3.8ms
[NexGo] GET  /not-found   404  0.4ms
[NexGo] GET  /api/crash   500  0.1ms recovered: nil pointer
MiddlewareDescription
RecoverCatches panics, returns 500
LoggerLogs method, path, status, duration
SecurityHeadersX-Content-Type-Options, X-Frame-Options, etc.
GzipCompresses responses (configurable)
CORS(origins...)Configurable CORS headers
Cache(maxAge)Sets Cache-Control headers

Hot Reload (HMR)

Run nexgo dev β€” file watcher polls every 500ms, broadcasts reload via SSE to all connected browsers.

nexgo dev
Terminal Output β€” Hot Reload
[NexGo] Dev server β†’ http://localhost:3000
[NexGo] Hot reload enabled. Watching 12 files…
[NexGo] Press Ctrl+C to stop.

β€” you edit pages/about.html β€”

[HMR] Change detected: pages/about.html
[HMR] βœ“ Browser reloaded (12ms)

DevTools Panel

In dev mode, visit http://localhost:3000/_nexgo/devtools:

TabShows
RoutesAll routes with type badges (PAGE / API)
RequestsLive log with method, status, path, duration
LogsServer logs, HMR events, errors
PerformanceTotal requests, avg response time, error rate
ConfigCurrent server and build configuration

Building for Production

nexgo build
nexgo start
nexgo start --port 4000
Terminal Output β€” Build & Start
$ nexgo build
[NexGo] Building static site…
βœ“ Compiled 8 pages
βœ“ Processed 3 API routes
βœ“ Copied static assets (14 files)
βœ“ Output β†’ .nexgo/out/ (234 KB)

$ nexgo start
[NexGo] Production server β†’ http://localhost:3000
[NexGo] Gzip enabled Β· HMR disabled

Deploying

NexGo compiles to a single binary β€” deploy anywhere Go runs:

# Build the binary
go build -o myapp ./main.go

# Copy to server
scp myapp server:/opt/myapp/
scp -r pages/ layouts/ static/ nexgo.config.json server:/opt/myapp/

# Run on server
ssh server '/opt/myapp/myapp'

Configuration

// nexgo.config.json
{
  "projectName": "my-app",
  "port": 3000,
  "host": "localhost",
  "pagesDir": "pages",
  "staticDir": "static",
  "layoutsDir": "layouts",
  "componentsDir": "components",
  "outputDir": ".nexgo/out",
  "hotReload": true,
  "compression": true,
  "minify": true,
  "defaultRenderMode": "ssr"
}

Template Functions Reference

String

FunctionUsageOutputDescription
upper{"{"}{ upper "hello" }{"}"}HELLOUppercase
lower{"{"}{ lower "HI" }{"}"}hiLowercase
title{"{"}{ title "hello world" }{"}"}Hello WorldTitle case
trim{"{"}{ trim " hi " }{"}"}hiTrim whitespace
replace{"{"}{ replace "hi" "i" "ey" }{"}"}heyReplace substring
split{"{"}{ split "a,b" "," }{"}"}[a b]Split to slice

Utility

FunctionDescription
json .DataJSON encode a value
safeHTML "<b>bold</b>"Render as raw HTML (trusted content only)
dict "key" "val"Create a key-value map
asset "/static/css/app.css"Cache-busted asset URL
default "fallback" .ValueDefault if nil/empty
times 5Iterate n times

API Helpers Reference

import "github.com/salmanfaris22/nexgo/pkg/api"

// Responses
api.JSON(w, data)               // 200 + JSON
api.JSONStatus(w, 201, data)    // Custom status
api.Error(w, 400, "bad input")  // JSON error

// Status shortcuts
api.BadRequest(w, "invalid id") // 400
api.NotFound(w, "not found")    // 404
api.Unauthorized(w)             // 401
api.InternalError(w, err)       // 500

// Request parsing
var input CreateUserInput
if !api.Decode(w, r, &input) { return }

// Method routing
api.Route(w, r, api.Methods{
    "GET":  listUsers,
    "POST": createUser,
})

// Pagination
page, limit := api.Paginate(r) // ?page=2&limit=10

HTMX Helpers

Send HTML fragments from Go and let HTMX update the DOM β€” no page reloads needed.

api.IsHTMX(r) β€” Check if request is from HTMX

if api.IsHTMX(r) {
    // Return HTML fragment
    api.HTMXHTML(w, "<li>New item</li>")
} else {
    // Return JSON
    api.JSON(w, data)
}

api.HTMXHTML(w, html) β€” Send HTML fragment response

api.HTMXHTML(w, "<div>Updated content</div>")
HTTP Response β€” HTMX fragment
HTTP/1.1 200 OK
Content-Type: text/html

<div>Updated content</div>
↳ HTMX swaps this into the target element β€” no page reload

api.HTMXTrigger(w, event) β€” Fire client-side event after response

api.HTMXTrigger(w, "todo-added")

api.HTMXHeader(w, key, value) β€” Set any HTMX header

api.HTMXHeader(w, "HX-Redirect", "/dashboard")

State Management

Thread-safe global state shared across all handlers. No mutex boilerplate.

api.SetState(key, value) β€” Store a value

api.SetState("todos", []Todo{...})
api.SetState("counter", 42)

api.GetState(key) β€” Retrieve a value

todos := api.GetState("todos").([]Todo)

api.DeleteState(key) β€” Remove a key

api.DeleteState("todos")

api.NewState() β€” Create isolated state (optional)

sessionState := api.NewState()
sessionState.Set("user", "alice")

HTML Helpers

api.Escape(s) β€” Sanitize HTML output

safe := api.Escape("<script>alert('xss')</script>")
Output
&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;

api.HTML(w, html) β€” Send raw HTML response

api.HTML(w, "<h1>Hello</h1>")

SEO (Meta, Sitemap, OpenGraph)

Meta tags, OpenGraph, Twitter Cards, sitemap.xml, robots.txt, and JSON-LD structured data.

Meta Tags

import "github.com/salmanfaris22/nexgo/pkg/seo"

meta := seo.DefaultMeta("My Site", "A great website", "https://mysite.com")
meta.Keywords = []string{"go", "web", "framework"}
meta.OGImage = "https://mysite.com/og.png"
meta.TwitterCard = "summary_large_image"
meta.TwitterSite = "@mysite"

// Use in template: {"{"}{ .MetaTags }{"}"}

Article Meta

meta := seo.ArticleMeta(
    "How to Build APIs in Go",
    "A guide to building fast APIs",
    "John Doe",
    "https://mysite.com/images/cover.jpg",
    "https://mysite.com/blog/go-apis",
    time.Now(),
)

Sitemap

entries := []seo.SitemapEntry{
    {Loc: "https://mysite.com/", ChangeFreq: "daily", Priority: 1.0},
    {Loc: "https://mysite.com/about", ChangeFreq: "monthly", Priority: 0.8},
    {Loc: "https://mysite.com/blog", ChangeFreq: "weekly", Priority: 0.9},
}
xml, _ := seo.RenderSitemap(entries)
// Write xml to /sitemap.xml response

Robots.txt

robots := seo.RobotsTxt(
    []string{"/"},                    // allow
    []string{"/admin", "/api"},       // disallow
    "https://mysite.com/sitemap.xml", // sitemap URL
)
// Write robots to /robots.txt response

JSON-LD Structured Data

data := map[string]interface{}{
    "@context": "https://schema.org",
    "@type": "Organization",
    "name": "My Company",
    "url": "https://mysite.com",
}
jsonld := seo.JSONLD(data)
// In template: {"{"}{ .JSONLD }{"}"}

Template Caching

Templates are compiled once and cached for fast subsequent renders.

import "github.com/salmanfaris22/nexgo/pkg/renderer"

r := renderer.New(cfg)
r.LoadAll()  // Compiles and caches all templates

// Check cache stats
info := r.CacheInfo()
// map[templates:5 layouts:1 components:3 loaded_at:2026-04-03...]

Parallel Template Loading

r.LoadParallel()  // Uses worker pool to compile templates concurrently

Parallel Data Loading

Load multiple data sources concurrently and merge results β€” like Promise.all() in JS.

import (
    "github.com/salmanfaris22/nexgo/pkg/renderer"
    "net/http"
)

// Create parallel loader
loader := renderer.NewParallelLoader().
    Add("users", fetchUsers).
    Add("posts", fetchPosts).
    Add("stats", fetchStats)

// Execute all concurrently
props, err := loader.Execute(req, params)
// props = map[string]interface{}{
//     "users": [...], "posts": [...], "stats": {...}
// }

func fetchUsers(req *http.Request, params map[string]string) (map[string]interface{}, error) {
    users := db.Query("SELECT * FROM users")
    return map[string]interface{}{"users": users}, nil
}

func fetchPosts(req *http.Request, params map[string]string) (map[string]interface{}, error) {
    posts := db.Query("SELECT * FROM posts LIMIT 10")
    return map[string]interface{}{"posts": posts}, nil
}

Streaming SSR

Send HTML progressively as it's ready β€” show the shell immediately, stream content later.

import "github.com/salmanfaris22/nexgo/pkg/stream"

func handlePage(w http.ResponseWriter, r *http.Request) {
    sw := stream.StreamHTML(w)

    // Send head + layout immediately
    sw.WriteString(`<!DOCTYPE html><html><head><title>My Page</title></head><body>`)
    sw.WriteString(`<header>Navigation</header><main>`)
    sw.Flush()

    // Do slow work (DB query, API call)
    data := slowDatabaseQuery()

    // Stream content
    sw.WriteString(fmt.Sprintf("<h1>%s</h1>", data.Title))
    sw.WriteString(fmt.Sprintf("<p>%s</p>", data.Body))

    // Close HTML
    sw.WriteString(`</main></body></html>`)
}

Server-Sent Events (SSE)

func handleSSE(w http.ResponseWriter, r *http.Request) {
    sse := stream.NewSSE(w)
    defer sse.Flush()

    for {
        sse.Send("update", `{"count": 42}`)
        sse.Ping()
        time.Sleep(1 * time.Second)
    }
}

Response Caching

Cache GET responses in memory with TTL. Automatic cache headers.

Middleware

import "github.com/salmanfaris22/nexgo/pkg/api"

// Cache a handler for 5 minutes
mux.HandleFunc("/api/posts", api.Cache(5*time.Minute)(handlePosts))

// Cache with custom TTL
mux.HandleFunc("/api/slow", api.Cache(30*time.Second)(handleSlow))

Manual Cache

// Set cache
api.CacheSet("/api/data", 200, headers, body)

// Get cache
code, headers, body, ok := api.CacheGet("/api/data")
if ok {
    w.WriteHeader(code)
    w.Write(body)
    return
}

// Delete specific key
api.CacheDelete("/api/data")

// Clear all
api.CacheClear()
Response Headers β€” Cache status
First request:   X-Cache: MISS  β€” generated fresh (45ms)
Second request:  X-Cache: HIT   β€” served from cache (0.2ms)
After TTL:       X-Cache: MISS  β€” cache expired, regenerated

Worker Pools

Run tasks concurrently with controlled parallelism.

Basic Pool

import "github.com/salmanfaris22/nexgo/pkg/worker"

pool := worker.New(4)  // 4 workers
pool.Start()

pool.Submit(func() error {
    return processFile("data1.csv")
})
pool.Submit(func() error {
    return processFile("data2.csv")
})

errs := pool.Wait()  // blocks until all done

Map (concurrent transform)

results := worker.Map(4, items, func(item Item) Result {
    return process(item)
})
// results are in original order

Map with Errors

results, err := worker.MapErr(4, urls, func(url string) (Page, error) {
    return fetchPage(url)
})

ForEach

worker.ForEach(8, emails, func(email string) {
    sendEmail(email)
})

Global Pool

worker.Start()  // starts global pool with 4 workers
worker.Submit(func() error { return doWork() })
errs := worker.Wait()

Incremental Static Regeneration (ISR)

Cache pages and rebuild them in the background after a configurable interval β€” like Next.js ISR.

Basic ISR

import "github.com/salmanfaris22/nexgo/pkg/isr"

// Create ISR with 60-second revalidation
cache := isr.New(60 * time.Second)

func handlePage(w http.ResponseWriter, r *http.Request) {
    cache.Serve(w, r, func() (int, http.Header, []byte, error) {
        // This runs only on first request or after 60s
        html := renderExpensivePage()
        return 200, nil, []byte(html), nil
    })
}

How ISR Works

StepBehaviorHeader
First requestGenerates page, caches it, returnsX-ISR: MISS
Subsequent (within TTL)Serves cached pageX-ISR: STALE
After TTL expiresServes stale, regenerates in backgroundX-ISR: REVALIDATING
Next requestGets fresh cached contentX-ISR: HIT

Manual Revalidation

// Trigger background rebuild of a specific path
cache.Revalidate("/blog/post-1", func() (int, http.Header, []byte, error) {
    return 200, nil, []byte(renderPost()), nil
})

// Purge from cache
cache.Purge("/blog/post-1")
cache.PurgeAll()

Global ISR

isr.SetRevalidate(120 * time.Second)

isr.Serve(w, r, func() (int, http.Header, []byte, error) {
    return 200, nil, []byte(html), nil
})

isr.Revalidate("/page", generateFunc)
isr.Purge("/page")

Environment Variables

The pkg/env package loads .env files with priority-based overrides and provides type-safe access to environment variables. Files are loaded in order: .env, .env.local, .env.{mode}, with later files taking precedence.

import "github.com/salmanfaris22/nexgo/pkg/env"

// Load .env files (call once at startup)
env.LoadGlobal()              // loads .env, .env.local, .env.development
env.LoadGlobal("production")  // loads .env, .env.local, .env.production

// Create a store for typed access
store := env.NewStore()
store.Get("DATABASE_URL")              // returns value or ""
store.MustGet("SECRET_KEY")            // panics if missing
store.GetInt("PORT", 3000)             // with default
store.GetBool("DEBUG", false)
Example .env files & loaded values
# .env
DATABASE_URL=postgres://localhost/myapp
PORT=3000

# .env.production
DATABASE_URL=postgres://prod-db/myapp  ← overrides .env
SECRET_KEY=a1b2c3d4...

store.GetInt("PORT", 8080) β†’ 3000
store.GetBool("DEBUG", false) β†’ false

Sessions

Cookie-based session management with pluggable backends. Ships with MemoryStore (development) and FileStore (production). Supports flash messages for one-time notifications.

import "github.com/salmanfaris22/nexgo/pkg/session"

mgr := session.NewManager(session.Options{
    Store:    session.NewMemoryStore(),  // or session.NewFileStore("./sessions")
    MaxAge:   3600,
    HTTPOnly: true,
    Secure:   true,
})

func handler(w http.ResponseWriter, r *http.Request) {
    sess := mgr.Start(w, r)
    sess.Set("user", "alice")
    name := sess.Get("user")  // "alice"

    // Flash messages (auto-cleared after read)
    sess.Flash("success", "Item created!")
    msgs := sess.Flashes("success")  // ["Item created!"]
}
Session Cookie β€” Browser DevTools
Set-Cookie: nexgo_session=abc123def456;
  Path=/; HttpOnly; Secure; Max-Age=3600

Session Store:
  "user"  β†’ "alice"
  "flash:success" β†’ ["Item created!"] (cleared after read)

Authentication (JWT)

JWT token generation, verification, middleware, role-based access control, and password hashing via pkg/auth.

import "github.com/salmanfaris22/nexgo/pkg/auth"

// Generate a JWT
token, err := auth.GenerateToken(auth.Claims{
    UserID: "123",
    Role:   "admin",
}, "my-secret-key", 24*time.Hour)

// Verify a JWT
claims, err := auth.VerifyToken(token, "my-secret-key")

// Middleware: protect routes
mux.Handle("/dashboard", auth.RequireAuth("my-secret-key")(dashboardHandler))
mux.Handle("/admin", auth.RequireRole("admin", "my-secret-key")(adminHandler))

// Password hashing
hash, _ := auth.HashPassword("s3cret")
ok := auth.CheckPassword("s3cret", hash)  // true

CSRF Protection

Token-based CSRF protection via pkg/csrf. Generates a per-session token, validates it on POST/PUT/DELETE requests, and provides a template helper for forms.

import "github.com/salmanfaris22/nexgo/pkg/csrf"

// Apply middleware
handler := csrf.Protect("my-csrf-secret")(myHandler)

// In templates, use the helper to embed the token in forms:
// <form method="POST">
//   {"{"}{ csrfField }{"}"} <!-- renders hidden input -->
//   <button type="submit">Submit</button>
// </form>

// For AJAX, read the token from the meta tag or cookie:
// X-CSRF-Token: <token>

Rate Limiting

Protect endpoints from abuse with token bucket and sliding window algorithms via pkg/ratelimit. Apply as global middleware or per-route.

import "github.com/salmanfaris22/nexgo/pkg/ratelimit"

// Token bucket: 100 requests per minute per IP
limiter := ratelimit.PerMinute(100)
mux.Handle("/api/", limiter.Middleware()(apiHandler))

// Sliding window: 10 requests per second
strict := ratelimit.PerSecond(10)
mux.Handle("/api/login", strict.Middleware()(loginHandler))

// Custom configuration
limiter := ratelimit.New(ratelimit.Config{
    Rate:     50,
    Window:   time.Minute,
    Strategy: ratelimit.SlidingWindow,
    KeyFunc:  func(r *http.Request) string { return r.RemoteAddr },
})

Internationalization (i18n)

Locale routing, JSON translation files, RTL support, and automatic locale detection via pkg/i18n. Place translation files in locales/.

import "github.com/salmanfaris22/nexgo/pkg/i18n"

// locales/en.json: {"greeting": "Hello, {"{"}name{"}"}!", "nav.home": "Home"}
// locales/ar.json: {"greeting": "!{"{"}name{"}"} ,Ω…Ψ±Ψ­Ψ¨Ψ§", "nav.home": "Ψ§Ω„Ψ±Ψ¦ΩŠΨ³ΩŠΨ©"}

bundle := i18n.New(i18n.Config{
    DefaultLocale: "en",
    Locales:       []string{"en", "fr", "ar"},
    Directory:     "locales",
    RTLLocales:    []string{"ar", "he"},
})

// In handlers
t := bundle.T("en")
t.Get("greeting", map[string]string{"name": "World"}) // "Hello, World!"

// Locale detection middleware (Accept-Language, cookie, URL prefix)
mux.Handle("/", bundle.Middleware()(handler))

Image Optimization

Automatic responsive images, lazy loading, srcset generation, and blur placeholders via pkg/image.

import "github.com/salmanfaris22/nexgo/pkg/image"

opt := image.NewOptimizer(image.Config{
    Widths:      []int{320, 640, 1024, 1920},
    Quality:     80,
    LazyLoad:    true,
    Placeholder: image.BlurPlaceholder,
})

// In templates, generates <img> with srcset, sizes, loading="lazy"
// {"{"}{ optimizeImage "/static/hero.jpg" "Hero image" }{"}"}
<!-- Output -->
<img src="/static/hero.jpg"
     srcset="/static/hero-320w.jpg 320w, /static/hero-640w.jpg 640w, /static/hero-1024w.jpg 1024w"
     sizes="(max-width: 640px) 100vw, 50vw"
     loading="lazy"
     alt="Hero image">

WebSocket

Native WebSocket support using HTTP hijacking via pkg/websocket. Includes a Hub for managing rooms and broadcasting messages.

import "github.com/salmanfaris22/nexgo/pkg/websocket"

hub := websocket.NewHub()
go hub.Run()

mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
    conn, err := websocket.Upgrade(w, r)
    if err != nil { return }
    client := hub.Register(conn)
    defer hub.Unregister(client)

    // Join a room
    hub.Join(client, "chat-room-1")

    // Broadcast to all clients
    hub.Broadcast([]byte("Hello everyone!"))

    // Broadcast to a specific room
    hub.BroadcastTo("chat-room-1", []byte("Room message"))
})

Health Checks

Kubernetes-compatible health check endpoints via pkg/health. Exposes /health, /ready, and /live with dependency checks.

import "github.com/salmanfaris22/nexgo/pkg/health"

checker := health.New()

// Register dependency checks
checker.AddCheck("database", func() error {
    return db.Ping()
})
checker.AddCheck("redis", func() error {
    return redis.Ping()
})

// Mount endpoints
mux.Handle("/health", checker.Handler())    // overall status
mux.Handle("/ready", checker.ReadyHandler()) // readiness probe
mux.Handle("/live", checker.LiveHandler())   // liveness probe

// GET /health β†’ {"status":"healthy","checks":{"database":"ok","redis":"ok"}}
API Response β€” GET /health
HTTP/1.1 200 OK

{
  "status": "healthy",
  "checks": {
    "database": "ok",
    "redis":    "ok"
  },
  "uptime": "2h 14m 32s"
}

Structured Logging

Leveled, structured logging with JSON output, colored console mode, and file rotation via pkg/logger.

import "github.com/salmanfaris22/nexgo/pkg/logger"

log := logger.New(logger.Config{
    Level:    logger.Info,
    Format:   logger.JSON,       // or logger.Console (colored)
    Output:   "logs/app.log",    // file path or "stdout"
    Rotation: logger.Daily,      // Daily, Hourly, or by Size
})

log.Info("server started", "port", 3000)
log.Error("query failed", "err", err, "query", sql)
log.Debug("request", "method", r.Method, "path", r.URL.Path)

// Request logger middleware
mux.Handle("/", logger.RequestMiddleware(log)(handler))
Log Output β€” JSON format
{"level":"info","msg":"server started","port":3000}
{"level":"info","method":"GET","path":"/","status":200,"duration":"1.2ms"}
{"level":"error","msg":"query failed","err":"connection refused"}

Metrics

Prometheus-compatible metrics with counters, gauges, and histograms via pkg/metrics. Includes built-in HTTP middleware and a /metrics endpoint.

import "github.com/salmanfaris22/nexgo/pkg/metrics"

m := metrics.New()

// Define metrics
requestCount := m.Counter("http_requests_total", "Total HTTP requests")
activeConns := m.Gauge("http_active_connections", "Active connections")
latency := m.Histogram("http_request_duration_seconds", "Request latency")

// Use in handlers
requestCount.Inc()
activeConns.Set(42)
latency.Observe(0.023)

// Expose /metrics endpoint (Prometheus scrape target)
mux.Handle("/metrics", m.Handler())

// Auto-instrument all HTTP traffic
mux.Handle("/", m.Middleware()(handler))
Output β€” GET /metrics
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total 1247

# HELP http_active_connections Active connections
# TYPE http_active_connections gauge
http_active_connections 42

# HELP http_request_duration_seconds Request latency
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_sum 28.6
http_request_duration_seconds_count 1247

Database

Lightweight data layer via pkg/db with a JSON file database for prototyping and a SQL query builder for production.

import "github.com/salmanfaris22/nexgo/pkg/db"

// JSON file database (zero setup)
store := db.NewJSONStore("data/")
store.Insert("users", map[string]interface{}{"name": "Alice", "age": 30})
users := store.FindAll("users")
store.Update("users", "id-123", map[string]interface{}{"age": 31})
store.Delete("users", "id-123")

// SQL query builder
q := db.Table("users").
    Where("age > ?", 18).
    OrderBy("name ASC").
    Limit(10).
    SelectSQL()
Generated SQL
SELECT * FROM users WHERE age > 18 ORDER BY name ASC LIMIT 10

Migrations

Version-tracked database migrations with up/down support via pkg/migrate.

import "github.com/salmanfaris22/nexgo/pkg/migrate"

mgr := migrate.New(db)

mgr.RegisterSQL("001_create_users",
    "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT)",
    "DROP TABLE users",
)
mgr.RegisterSQL("002_add_role",
    "ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'",
    "ALTER TABLE users DROP COLUMN role",
)

mgr.Up()       // run all pending migrations
mgr.Down()     // rollback last migration
mgr.Status()   // list applied and pending
mgr.Pending()  // list unapplied migrations
Terminal Output β€” Migrations
$ mgr.Up()
βœ“ Applied 001_create_users (2ms)
βœ“ Applied 002_add_role (1ms)

$ mgr.Status()
  βœ“ 001_create_users  applied 2026-04-06 10:23:01
  βœ“ 002_add_role      applied 2026-04-06 10:23:01

Plugin System

Hook-based plugin architecture via pkg/plugin. Plugins can add middleware, routes, template functions, and respond to lifecycle events.

import "github.com/salmanfaris22/nexgo/pkg/plugin"

// Define a plugin
myPlugin := plugin.New("analytics", func(p *plugin.Plugin) {
    // Add middleware to all routes
    p.AddMiddleware(trackingMiddleware)

    // Add a new route
    p.AddRoute("GET", "/analytics", analyticsHandler)

    // Add template functions
    p.AddTemplateFunc("trackEvent", func(name string) string {
        return fmt.Sprintf(`<script>track("%s")</script>`, name)
    })

    // Lifecycle hooks
    p.OnStart(func() { log.Println("Analytics plugin started") })
    p.OnShutdown(func() { flushEvents() })
})

// Register with the app
app.Use(myPlugin)

Server Actions

Form-based server mutations with built-in validation via pkg/actions. Similar to Next.js Server Actions but using standard HTML forms.

import "github.com/salmanfaris22/nexgo/pkg/actions"

actions.Register("createPost", func(ctx *actions.Context) error {
    title := ctx.FormValue("title")
    body := ctx.FormValue("body")

    // Validation
    ctx.Validate("title", title != "", "Title is required")
    ctx.Validate("body", len(body) > 10, "Body must be at least 10 chars")
    if ctx.HasErrors() { return ctx.ValidationError() }

    db.Insert("posts", map[string]interface{}{"title": title, "body": body})
    return ctx.RedirectTo("/blog")
})
<!-- In templates -->
<form method="POST" action="/actions/createPost">
  {"{"}{ actionField "createPost" }{"}"} <!-- hidden input with action name -->
  <input name="title" type="text">
  <textarea name="body"></textarea>
  <button type="submit">Create Post</button>
</form>

Error Pages

Convention-based error pages via pkg/errorpage. Place 404.html or 500.html in pages/ or any subdirectory for per-section error boundaries.

pages/
β”œβ”€β”€ 404.html              # Global 404
β”œβ”€β”€ 500.html              # Global 500
└── admin/
    └── 404.html          # Admin-specific 404
import "github.com/salmanfaris22/nexgo/pkg/errorpage"

// Apply middleware (auto-discovers error pages)
handler := errorpage.Middleware("pages")(mux)

// Error pages receive:
// {"{"}{ .StatusCode }{"}"} β€” 404, 500, etc.
// {"{"}{ .Message }{"}"} β€” human-readable error message
// {"{"}{ .Path }{"}"} β€” the requested URL path

Testing

Test client with HTTP method helpers and assertions via pkg/testing. Spin up your app in-process and make real HTTP requests.

import nextest "github.com/salmanfaris22/nexgo/pkg/testing"

func TestHomePage(t *testing.T) {
    app := nextest.NewClient(myHandler)

    // GET request
    resp := app.GET("/")
    resp.AssertStatus(200)
    resp.AssertContains("Welcome")

    // POST with form data
    resp = app.POST("/api/users", nextest.Form{"name": "Alice", "email": "a@b.com"})
    resp.AssertStatus(201)
    resp.AssertJSON("name", "Alice")

    // PUT / DELETE
    app.PUT("/api/users/1", nextest.JSON{"name": "Bob"}).AssertStatus(200)
    app.DELETE("/api/users/1").AssertStatus(204)
}

// Benchmarking
func BenchmarkAPI(b *testing.B) {
    app := nextest.NewClient(myHandler)
    nextest.Bench(b, func() { app.GET("/api/data") })
}

Per-Route Middleware

Apply middleware to individual routes or groups via pkg/middleware. Includes built-in timeout, request ID, and CSP middleware with nonce support.

import "github.com/salmanfaris22/nexgo/pkg/middleware"

// Per-route middleware
mux.Handle("/api/slow", middleware.RouteMiddleware(
    middleware.Timeout(5*time.Second),
    middleware.RequestID(),
)(slowHandler))

// Route groups
api := middleware.RouteGroup("/api",
    middleware.Timeout(10*time.Second),
    middleware.RequestID(),
)
api.Handle("/users", usersHandler)
api.Handle("/posts", postsHandler)

// CSP with nonce (auto-injected into templates as .CSPNonce)
mux.Handle("/", middleware.CSP(middleware.CSPConfig{
    ScriptSrc: "'nonce-{"{"}nonce{"}"}'",
    StyleSrc:  "'self'",
})(handler))

Deployment Adapters

One-command deployment to 8 platforms via pkg/deploy. Each adapter generates the necessary config files (Dockerfile, serverless handler, etc.).

import "github.com/salmanfaris22/nexgo/pkg/deploy"

// Generate deployment config
deploy.Generate("docker")       // Dockerfile + docker-compose.yml
deploy.Generate("vercel")       // vercel.json
deploy.Generate("cloudflare")   // wrangler.toml
deploy.Generate("lambda")       // serverless.yml + handler wrapper
deploy.Generate("netlify")      // netlify.toml
deploy.Generate("fly")          // fly.toml
deploy.Generate("railway")      // railway.json
deploy.Generate("k8s")          // deployment.yaml + service.yaml
# CLI shortcut
nexgo deploy docker      # generates Dockerfile and builds image
nexgo deploy vercel      # generates vercel.json and deploys
nexgo deploy k8s         # generates K8s manifests
Terminal Output β€” nexgo deploy docker
[NexGo] Generating Docker deployment…
βœ“ Created Dockerfile
βœ“ Created docker-compose.yml
βœ“ Created .dockerignore

[NexGo] Building image my-app:latest…
βœ“ Image built (23.4 MB)

Run with: docker compose up

Redis Cache

Zero-dependency Redis client using raw RESP protocol via pkg/cache. Includes connection pooling, key prefixing, and a CacheAdapter interface for swappable backends.

import "github.com/salmanfaris22/nexgo/pkg/cache"

rc := cache.NewRedis(cache.RedisConfig{
    Addr:      "localhost:6379",
    Password:  "",
    DB:        0,
    PoolSize:  10,
    KeyPrefix: "myapp:",
})

rc.Set("user:123", userData, 5*time.Minute)
val, err := rc.Get("user:123")
rc.Delete("user:123")

// Use as CacheAdapter (swap in-memory / Redis without code changes)
var store cache.CacheAdapter = rc
store.Set("key", value, ttl)
store.Get("key")

Asset Pipeline

CSS and JS minification, bundling, fingerprinting, and critical CSS extraction via pkg/asset.

import "github.com/salmanfaris22/nexgo/pkg/asset"

pipeline := asset.New(asset.Config{
    SourceDir:  "static",
    OutputDir:  ".nexgo/assets",
    Minify:     true,
    Bundle:     true,
    Fingerprint: true,
})

// Process all assets
pipeline.Build()

// In templates, reference fingerprinted URLs:
// <link rel="stylesheet" href="{"{"}{ asset "/css/app.css" }{"}"}">
// β†’ <link rel="stylesheet" href="/static/css/app.a1b2c3d4.css">

// Critical CSS (inline above-the-fold styles)
critical := pipeline.ExtractCritical("pages/index.html")
// Inject into <head> as <style>...</style>

ORM

Lightweight ORM with model definitions, auto-migration SQL generation, CRUD operations (JSON backend), and a fluent query DSL via pkg/orm.

import "github.com/salmanfaris22/nexgo/pkg/orm"

// Define a model
type User struct {
    orm.Model
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

db := orm.New("data/")
db.AutoMigrate(&User{})  // generates CREATE TABLE SQL

// CRUD
db.Create(&User{Name: "Alice", Email: "alice@example.com", Age: 30})

var user User
db.Find(&user, "id = ?", 1)

db.Where("age > ?", 18).Order("name").Limit(10).FindAll(&users)

// Fluent query with joins
db.Table("users").
    Join("posts", "users.id = posts.user_id").
    Where("posts.published = ?", true).
    Select("users.name", "COUNT(posts.id) as post_count").
    GroupBy("users.name").
    QuerySQL()
Generated SQL β€” Join query
SELECT users.name, COUNT(posts.id) AS post_count
FROM users
JOIN posts ON users.id = posts.user_id
WHERE posts.published = true
GROUP BY users.name

Cluster Mode

Run your app across multiple worker goroutines with graceful shutdown and zero-downtime restart via pkg/cluster. The built-in load balancer distributes requests across workers.

import "github.com/salmanfaris22/nexgo/pkg/cluster"

c := cluster.New(cluster.Config{
    Workers:         4,                  // number of worker goroutines
    GracefulTimeout: 30 * time.Second,   // max shutdown wait
    Handler:         mux,
    Addr:            ":3000",
})

// Start with automatic load balancing
c.Start()

// Zero-downtime restart (new workers start before old ones stop)
// Send SIGUSR2 to trigger:
// kill -USR2 <pid>

// Graceful shutdown on SIGINT/SIGTERM
// Workers finish in-flight requests before exiting
New in v2.2.1

SEO Killer Mode

The most SEO-complete Go framework. Every feature works out-of-the-box with zero configuration. Auto sitemap, robots.txt, canonical URLs, 10 JSON-LD schema types, OpenGraph, Twitter Cards, RSS feeds, Core Web Vitals, hreflang, SEO audit, and more.

22+ Functions 10 Schema Types Zero Config

Quick Start

Add SEO to any NexGo app in 2 steps:

// 1. Add to nexgo.config.json
"seo": { "siteName": "My Site", "siteURL": "https://example.com", "autoSitemap": true }

// 2. Add to layouts/default.html
{{ seoTags (seoMeta .Title .Description .Canonical) }}
{{ breadcrumbs .Path }}

That's it. You now have: auto sitemap.xml, robots.txt, canonical URLs, OpenGraph, Twitter Cards, breadcrumb schema, and SEO meta tags on every page.

SEO Template Functions (Reference)

FunctionUsageDescription
seoTags{{ seoTags (seoMeta .Title .Description .Canonical) }}Render all SEO meta tags
seoMeta{{ seoMeta "Title" "Desc" "/url" }}Create a Meta struct
breadcrumbs{{ breadcrumbs .Path }}Auto BreadcrumbList JSON-LD
jsonld{{ jsonld (dict "@type" "Event" ...) }}Generic JSON-LD tag
websiteSchema{{ websiteSchema "Name" "URL" "SearchURL" }}WebSite schema
orgSchema{{ orgSchema "Name" "URL" "Logo" }}Organization schema
faqSchema{{ faqSchema "Q?" "A." "Q?" "A." }}FAQ schema
preload{{ preload "/path" "style" }}Resource preload hint
vitals{{ vitals }}Core Web Vitals script
slugify{{ slugify "Hello World" }}SEO-friendly URL slug

SEO Configuration

All SEO settings live in nexgo.config.json under the "seo" key:

{
  "seo": {
    "siteName": "My Awesome Site",
    "siteURL": "https://example.com",
    "titleTemplate": "%s | My Awesome Site",
    "defaultOGImage": "https://example.com/static/images/og-default.png",
    "twitterSite": "@mysite",
    "language": "en",
    "autoSitemap": true,
    "autoRobotsTxt": true,
    "autoCanonical": true,
    "coreWebVitals": false,
    "robotsDisallow": ["/api/", "/_nexgo/", "/admin/"],
    "redirects": [
      {"from": "/old-page", "to": "/new-page", "status": 301}
    ]
  }
}

Auto Sitemap

NexGo automatically generates /sitemap.xml from your route table. No configuration needed.

Includes <lastmod>, <changefreq>, <priority> tags. Homepage gets priority 1.0. API and dynamic routes are excluded. Supports sitemap index for 50k+ URLs and hreflang alternates in sitemap.

// Custom sitemap with dynamic routes
entries := seo.AutoSitemap("https://example.com", staticRoutes, 0.5)
for _, post := range posts {
    entries = append(entries, seo.SitemapEntry{
        Loc:        "https://example.com/blog/" + post.Slug,
        LastMod:    post.UpdatedAt.Format("2006-01-02"),
        ChangeFreq: "monthly",
        Priority:   0.7,
    })
}

Auto Robots.txt

Auto-served at /robots.txt based on your config. Supports multiple user-agent blocks, crawl delay, and AI bot blocking (GPTBot, CCBot).

content := seo.RobotsTxtAdvanced([]seo.RobotsRule{
    {UserAgent: "*", Allow: []string{"/"}, Disallow: []string{"/api/"}},
    {UserAgent: "GPTBot", Disallow: []string{"/"}},
}, []string{"https://example.com/sitemap.xml"})

Meta Tags & Title Templates

Use seoTags in your layout to generate all SEO meta tags in one call:

<head>
  {{ seoTags (seoMeta .Title .Description .Canonical) }}
</head>

Generates: viewport, title (with template), description, og:title/description/url/image/type/site_name/locale, twitter:card/title/description/image/site, canonical, theme-color, and favicon.

OpenGraph & Twitter Cards

Full support for all OG types (website, article, product, profile) and Twitter cards (summary, summary_large_image). Article-specific tags: article:published_time, article:modified_time, article:author, article:section, article:tag.

meta := seo.ArticleMeta(
    "How to Build Fast Web Apps",
    "A guide to building performant apps...",
    "Salman Faris",
    "https://example.com/img/article.png",
    "https://example.com/blog/fast-apps",
    time.Now(),
)

JSON-LD Structured Data

10 pre-built structured data types plus a generic builder:

  • WebSite β€” with Sitelinks Searchbox
  • Organization β€” name, logo, social profiles
  • LocalBusiness β€” address, phone, coordinates
  • Article β€” author, publisher, dates, tags
  • BreadcrumbList β€” auto-generated from URL path
  • FAQ β€” question/answer pairs
  • Product β€” price, SKU, availability, reviews
  • Review β€” author, rating, body
  • HowTo β€” step-by-step instructions
  • Generic β€” any JSON-LD via jsonld template func
// In templates
{{ breadcrumbs .Path }}
{{ faqSchema "What is NexGo?" "A Go web framework." "Is it fast?" "200k+ req/sec." }}
{{ websiteSchema "My Site" "https://example.com" "https://example.com/search?q=" }}

RSS Feed Generation

Built-in RSS 2.0 renderer. Register an endpoint, build your feed items, and serve:

data, _ := seo.RenderRSS(seo.Feed{
    Title:       "My Site Blog",
    Link:        "https://example.com",
    Description: "Latest articles",
    Language:    "en",
    Items:       items,
})
w.Header().Set("Content-Type", "application/rss+xml")
w.Write(data)

Hreflang Alternates

For multilingual sites, add alternate language links. Supports x-default and hreflang in both HTML and sitemap:

meta.Alternates = []seo.AlternateLink{
    {HrefLang: "en", Href: "https://example.com/about"},
    {HrefLang: "fr", Href: "https://example.com/fr/about"},
    {HrefLang: "x-default", Href: "https://example.com/about"},
}

Core Web Vitals

Track LCP, CLS, and INP automatically. Enable in config, add {{ vitals }} to your layout, and create an API endpoint to receive beacon reports.

// nexgo.config.json
"coreWebVitals": true,
"vitalsEndpoint": "/api/vitals"

// In layout
{{ vitals }}

SEO Audit & Validation

Programmatic auditing catches SEO regressions in tests:

results := seo.AuditMeta(meta)
// Checks: MISSING_TITLE, TITLE_TOO_LONG, TITLE_TOO_SHORT,
// MISSING_DESCRIPTION, DESC_TOO_LONG, DESC_TOO_SHORT,
// MISSING_CANONICAL, MISSING_OG_IMAGE, NO_TWITTER_CARD

htmlResults := seo.AuditHTML(htmlString)
// Checks: MISSING_H1, MULTIPLE_H1, IMG_MISSING_ALT, MISSING_VIEWPORT

Redirects & Middleware

Config-based 301/302 redirects, trailing slash normalization, HSTS, and SEO HTTP headers:

// Config-based redirects
"redirects": [
    {"from": "/old-blog", "to": "/blog", "status": 301}
]

// Trailing slash normalization
handler := seo.TrailingSlashMiddleware(false, yourHandler)

// HSTS (1 year)
handler := seo.HSTSMiddleware(31536000, yourHandler)

// Auto SEO headers: Content-Language, X-Content-Type-Options,
// Referrer-Policy, Permissions-Policy
New in v2.2.0

High-Performance Mode

Push throughput from 50k to 200,000+ req/sec on multi-core machines β€” all without switching away from Go's standard net/http.

200k+ req/sec Cluster Mode Async Logging
ConfigThroughputLatency (p99)CPU Cores
v2.1.0 (default)~50k req/sec~2ms1
v2.2.0 (cluster, 4 cores)~200k req/sec~0.8ms4
v2.2.0 (cluster, 8 cores)~400k req/sec~0.5ms8
v2.2.0 (cluster + cache)~1M+ req/sec~0.05ms8 (cached)

Quick Start β€” Enable Performance Mode

// nexgo.config.json
{
  "clusterMode": true,
  "asyncLogging": true,
  "responseCache": true,
  "responseCacheTTL": 300,
  "compression": true
}
Terminal Output β€” nexgo start (cluster mode)
⚑ NexGo β€” Go-powered web framework

  Local:    http://localhost:3000
  Mode:     production
  Cluster:  4 workers (auto-detected 4 CPU cores)
  Cache:    response caching ON (TTL: 300s)
  Logging:  async (buffer: 8192)
  Islands:  3 (counter, search, chart)
  Pages:    ./pages/

Cluster Mode

Creates multiple http.Server instances that share a single TCP listener. The Go runtime distributes incoming connections across workers. Each worker has its own goroutine pool.

// Config
{
  "clusterMode": true,
  "clusterWorkers": 0    // 0 = auto-detect (runtime.NumCPU())
}

// Programmatic access
import "github.com/salmanfaris22/nexgo/v2/pkg/cluster"

stats := cluster.GetStats()
fmt.Printf("Active: %d, Total: %d, Uptime: %s\n",
    stats.ActiveRequests, stats.TotalRequests, stats.Uptime)
MachineWorkersExpected Throughput
2 vCPU (t3.small)2~100k req/sec
4 vCPU (t3.medium)4~200k req/sec
8 vCPU (c5.2xlarge)8~400k req/sec
16 vCPU (c5.4xlarge)16~750k+ req/sec

Zero-downtime restart: New workers start with updated handler, old workers drain in-flight requests (up to 30s), then shut down. No connections dropped.

Async Logging

Writes to a buffered channel (capacity: 8,192 entries). A background goroutine drains the channel. Requests are never blocked by log I/O.

// Config
{
  "asyncLogging": true
}

// Behavior at high load:
// - Normal: logs every request
// - Buffer full: drops log entry, request continues unblocked
// - Crash: up to 8,192 entries may be lost

Note: Async logging is only active in production mode. Dev mode always uses synchronous logging for accurate debugging.

Response Caching

Stores complete HTTP responses (status + headers + body) in memory. Cache hits return in microseconds.

// Config
{
  "responseCache": true,
  "responseCacheTTL": 300   // seconds
}

// Programmatic invalidation
import "github.com/salmanfaris22/nexgo/v2/pkg/cache"

cache.CacheDelete(cache.Key(req))             // delete specific
cache.GlobalCache().DeletePrefix("/blog")     // delete by prefix
cache.CacheClear()                            // clear all
Content TypeRecommended TTLWhy
Marketing pages3600s (1 hour)Rarely changes
Blog posts300s (5 min)Updates after publish
User dashboards0 (disabled)Personalized content
API responses30-60sData freshness

Headers: X-Cache: HIT or X-Cache: MISS on every response. Only GET requests are cached.

Route Match Caching

Caches route match results in a sync.Map β€” a lock-free concurrent map. After the first request to a path, subsequent requests skip regex matching entirely.

// First request to /about:
//   β†’ Regex scan (20 patterns) β†’ Match found β†’ Cache β†’ 800ns

// Second request to /about:
//   β†’ sync.Map lookup β†’ Cache hit β†’ 15ns

// Automatic β€” no configuration needed
// Cache clears on: route rescan (dev), server restart

Atomic Template Reads

After templates load, NexGo publishes an immutable snapshot via sync/atomic.Value. Production reads use this snapshot with zero locking.

Concurrent RequestsRWMutex LookupAtomic Lookup
100~50ns~5ns
1,000~200ns (contention)~5ns
10,000~800ns (high contention)~5ns

Atomic reads are O(1) constant time regardless of concurrency level. Always enabled β€” no config needed.

Optimization Summary

OptimizationImpactConfig
Cluster mode4-8x throughputclusterMode: true
Async logging10-15% gainasyncLogging: true
Route caching5-10% gainAlways on
Atomic templates5% gainAlways on
Response caching10-100x for cachedresponseCache: true
Demo β€” Benchmark Results
$ wrk -t8 -c256 -d30s http://localhost:3000/

Running 30s test @ http://localhost:3000/
  8 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     0.81ms   0.35ms   8.21ms   89.23%
    Req/Sec    25.4k    1.2k    28.9k    72.15%
  203,472 requests in 30.00s
Requests/sec: 203,472.18
Transfer/sec:      9.82MB
New in v2.1.0

Islands Architecture

Ship zero JavaScript by default. Only interactive components ("islands") load JS β€” with fine-grained control over when they hydrate.

0 KB Default 5 Strategies ~1KB Runtime

How it works: Pages are static HTML. Only interactive components ("islands") ship JavaScript to the browser. Each island has a server-side template (.html) and an optional client-side script (.js). The JS is only loaded when the island needs to hydrate β€” based on the strategy you choose.

MetricNext.js (React)NexGo Islands
JS on a static page~85KB (React runtime)0 KB
JS for 1 interactive button~85KB + component~2KB (just the island)
Time to Interactive (blog)~1.2s~0.1s
Hydration modelFull pagePer-component
JS loading controlNone5 strategies

Creating Islands

An island is a pair of files in the islands/ directory:

FilePurposeRequired
islands/name.htmlServer-side Go templateYes
islands/name.jsClient-side JavaScript (ES module)No (SSR-only if omitted)

Step 1: Create the Template

<!-- islands/counter.html -->
<div class="counter">
  <span class="counter-value">{{ .count }}</span>
  <button class="counter-btn" data-action="decrement">-</button>
  <button class="counter-btn" data-action="increment">+</button>
</div>

Step 2: Create the Client JS

// islands/counter.js
export default function init(el, props) {
  let count = props.count || 0;
  const display = el.querySelector('.counter-value');

  el.querySelectorAll('.counter-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      count += btn.dataset.action === 'increment' ? 1 : -1;
      display.textContent = count;
    });
  });
}

Rules for island JS:

  • Export a default function or a named init function
  • Receives two arguments: el (the <nexgo-island> DOM element) and props
  • Use vanilla JS β€” no framework required (but you can import one)

Step 3: Use in a Page

<!-- pages/index.html -->
<h1>My Page</h1>
<p>This is static HTML. Zero JavaScript.</p>

{{ island "counter" (props "count" 0) "client:load" }}
Generated HTML Output
<h1>My Page</h1>
<p>This is static HTML. Zero JavaScript.</p>

<nexgo-island data-name="counter"
              data-strategy="client:load"
              data-has-js="true"
              data-props='{"count":0}'>
  <div class="counter">
    <span class="counter-value">0</span>
    <button data-action="decrement">-</button>
    <button data-action="increment">+</button>
  </div>
</nexgo-island>

Hydration Strategies

Hydration strategies control when an island's JavaScript loads and executes.

StrategyJS LoadedWhenBest For
client:loadImmediatelyPage loadAbove-the-fold interactive UI
client:visibleOn scrollEnter viewportBelow-the-fold widgets
client:idleWhen idleBrowser free timeNon-critical interactive UI
client:mediaOn matchMedia queryDevice-specific UI
client:noneNeverβ€”Reusable SSR-only components

client:load β€” Immediate

Hydrate immediately when the page loads. Use for above-the-fold interactive elements.

{{ island "search-bar" (props "placeholder" "Search...") "client:load" }}

client:visible β€” On Scroll

Hydrate when the island scrolls into the viewport (using IntersectionObserver with 200px margin).

{{ island "comment-form" (props "postId" .Params.slug) "client:visible" }}

client:idle β€” When Free

Hydrate when the browser is idle (using requestIdleCallback).

{{ island "analytics-widget" (props "data" .Props.stats) "client:idle" }}

client:media β€” Device-Specific

Hydrate only when a CSS media query matches. The island JS is never loaded on non-matching devices.

{{ island "mobile-menu" nil "client:media=(max-width:768px)" }}

client:none β€” SSR Only

Never hydrate. The island is rendered server-side and sent as static HTML. No JS is ever shipped.

{{ island "sidebar" (props "user" .State.user) "client:none" }}
Demo β€” Multiple Islands on One Page
<!-- pages/dashboard.html -->
<h1>Dashboard</h1>

{{ island "search" (props "endpoint" "/api/search") "client:load" }}
<!-- ↑ Loads JS immediately -->

<section class="summary">
  Revenue: $
</section>
<!-- ↑ Static HTML β€” 0 KB JS -->

{{ island "chart" (props "data" .Props.chartData) "client:visible" }}
<!-- ↑ Loads when scrolled into view -->

{{ island "notifications" (props "count" 5) "client:idle" }}
<!-- ↑ Loads when browser is idle -->

{{ island "mobile-filters" nil "client:media=(max-width:768px)" }}
<!-- ↑ Only loads on mobile -->

{{ island "activity" (props "events" .Props.events) "client:none" }}
<!-- ↑ SSR only β€” zero JS ever -->

Passing Props

Use the props template function to pass data from the server to an island.

Basic Props

{{ island "greeting" (props "name" "World" "emoji" "hi") }}

<!-- islands/greeting.html -->
<p>{{ .emoji }} Hello, {{ .name }}!</p>

// islands/greeting.js
export default function(el, props) {
  console.log(props.name);  // "World"
}

Props from Data Loaders

// main.go
srv.RegisterDataLoader("/dashboard", func(req *http.Request, params map[string]string) (map[string]interface{}, error) {
    return map[string]interface{}{
        "stats":    fetchStats(),
        "chartData": fetchChartData(),
    }, nil
})

<!-- pages/dashboard.html -->
<div>Revenue: $</div>

{{ island "chart" (props "data" .Props.chartData "type" "line") "client:visible" }}

Props from Route Parameters

<!-- pages/blog/[slug].html -->
{{ island "comment-section" (props "postSlug" .Params.slug) "client:visible" }}

No Props

{{ island "theme-toggle" }}
{{ island "scroll-to-top" nil "client:visible" }}

Writing Island JavaScript

Every island JS file exports a default function that receives the element and props.

Basic Structure

// islands/my-island.js
export default function init(el, props) {
  // el    β€” the <nexgo-island> DOM element
  // props β€” the serialized props from the template
}

DOM Queries (Scoped to Island)

// islands/tabs.js
export default function(el, props) {
  const buttons = el.querySelectorAll('[data-tab]');
  const panels = el.querySelectorAll('[data-panel]');

  buttons.forEach(btn => {
    btn.addEventListener('click', () => {
      const target = btn.dataset.tab;
      panels.forEach(p => p.hidden = p.dataset.panel !== target);
      buttons.forEach(b => b.classList.toggle('active', b === btn));
    });
  });
}

Fetching Data from API Routes

// islands/live-count.js
export default function(el, props) {
  const display = el.querySelector('.count');

  async function refresh() {
    const res = await fetch('/api/visitors/count');
    const data = await res.json();
    display.textContent = data.count;
  }

  refresh();
  setInterval(refresh, 5000);
}

Importing Third-Party Libraries (CDN)

// islands/chart.js
import Chart from 'https://cdn.jsdelivr.net/npm/chart.js@4/+esm';

export default function(el, props) {
  const canvas = el.querySelector('canvas');
  new Chart(canvas, {
    type: props.type || 'bar',
    data: props.data,
  });
}
Demo β€” Search Island with API
<!-- islands/search.html -->
<div class="search-box">
  <input type="text" class="search-input"
         placeholder="{{ .placeholder }}" />
  <div class="search-results"></div>
</div>

// islands/search.js
export default function(el, props) {
  const input = el.querySelector('.search-input');
  const results = el.querySelector('.search-results');
  let timeout;

  input.addEventListener('input', () => {
    clearTimeout(timeout);
    timeout = setTimeout(async () => {
      const q = input.value.trim();
      const res = await fetch(`/api/search?q=${"{"}q{"}"}`);
      const data = await res.json();
      results.innerHTML = data.items
        .map(i => `<a href="${"{"}i.url{"}"}">${"{"}i.title{"}"}</a>`)
        .join('');
    }, 300);
  });
}

<!-- Usage -->
{{ island "search" (props "placeholder" "Search...") "client:load" }}

SSR-Only Islands

Create islands without a .js file for reusable server-rendered components. No JavaScript is ever shipped.

<!-- islands/product-card.html β€” No .js file -->
<div class="product-card">
  <img src="{{ .image }}" alt="{{ .name }}" loading="lazy">
  <h3>{{ .name }}</h3>
  <p class="price">${{ .price }}</p>
  {{ if .sale }}<span class="badge">Sale</span>{{ end }}
</div>

<!-- pages/shop.html β€” No JS shipped, pure HTML -->
<div class="product-grid">
  {{ range .Props.products }}
    {{ island "product-card" (props "name" .Name "price" .Price "image" .Image "sale" .OnSale) "client:none" }}
  {{ end }}
</div>
Demo β€” Product Grid (SSR-Only, 0KB JS)
<div class="product-grid">
  <nexgo-island data-name="product-card" data-strategy="client:none">
    <div class="product-card">
      <img src="/img/headphones.jpg" loading="lazy">
      <h3>Wireless Headphones</h3>
      <p class="price">$79.99</p>
      <span class="badge">Sale</span>
    </div>
  </nexgo-island>
  <!-- ... more cards, all pure HTML -->
</div>

Total JS shipped: 0 KB

Island Runtime

The island runtime is a lightweight (~1KB) JavaScript file that handles discovery and hydration.

How It Works

  1. On page load, the runtime scans for <nexgo-island> elements
  2. For each island, it reads the data-strategy attribute
  3. Based on the strategy, it schedules hydration:
    • load β†’ runs immediately
    • visible β†’ sets up IntersectionObserver
    • idle β†’ calls requestIdleCallback
    • media β†’ listens to matchMedia
  4. When triggered, it dynamically imports the island's JS module
  5. The module's default export is called with (element, props)

Including the Runtime

<!-- Option A: Script tag (recommended) -->
<script src="/_nexgo/island-runtime.js" defer></script>

<!-- Option B: Inline via template function -->
{{ islandRuntime }}

Template Functions Reference

FunctionUsageDescription
island{{ island "name" }}Render with default strategy (client:load)
island{{ island "name" (props "k" "v") }}Render with props
island{{ island "name" (props "k" "v") "client:visible" }}Render with props and strategy
island{{ island "name" nil "client:idle" }}No props, custom strategy
props{{ props "key" "val" "key2" "val2" }}Build a props map for islands
islandRuntime{{ islandRuntime }}Inline the hydration runtime

HTTP Endpoints

EndpointDescription
/_nexgo/islands/{name}.jsServes individual island JS files
/_nexgo/island-runtime.jsServes the hydration runtime

Configuration

// nexgo.config.json
{
  "islandsDir": "islands",   // Directory containing island files (default)
  // ... other config unchanged
}

Project Structure with Islands

my-app/
β”œβ”€β”€ main.go
β”œβ”€β”€ nexgo.config.json
β”œβ”€β”€ pages/                    # File-based routes
β”‚   β”œβ”€β”€ index.html
β”‚   └── blog/[slug].html
β”œβ”€β”€ islands/                  # Island components (NEW)
β”‚   β”œβ”€β”€ counter.html          #   Server template
β”‚   β”œβ”€β”€ counter.js            #   Client JS (ES module)
β”‚   β”œβ”€β”€ search.html
β”‚   β”œβ”€β”€ search.js
β”‚   β”œβ”€β”€ product-card.html     #   SSR-only (no .js)
β”‚   └── chart.html
β”‚       chart.js
β”œβ”€β”€ layouts/
β”œβ”€β”€ components/
β”œβ”€β”€ static/
└── .nexgo/out/
Terminal Output β€” nexgo dev with Islands
  ⚑ NexGo β€” Go-powered web framework

  Local:    http://localhost:3000
  Mode:     development (hot reload on)
  Pages:    ./pages/
  Islands:  3 (counter, search, chart)
  Devtools: http://localhost:3000/_nexgo/devtools

Migration from v2.0.x

No breaking changes. Islands are purely additive. Steps to start using them:

  1. Update NexGo: go get github.com/salmanfaris22/nexgo/v2@v2.1.0
  2. Create islands/ directory: mkdir islands
  3. Add the runtime to your layout: <script src="/_nexgo/island-runtime.js" defer></script>
  4. Start creating islands!

CLI Commands

CommandDescription
nexgo create <name>Scaffold a new NexGo project
nexgo devStart dev server with hot reload
nexgo buildStatic site generation (SSG)
nexgo startStart production server
nexgo versionPrint version
nexgo helpShow help
Terminal Output β€” nexgo version
NexGo v2.2.0
Go    go1.22.5 linux/arm64
Built 2026-04-06
Islands enabled
Cluster 4 workers

πŸ’¬ Need help? Open an issue on GitHub, or reach out to salmanfariskalm@gmail.com.