{{.Post.Title}}
{{.Post.Date}}
{{.Post.Content}}
This is the home page.
{{end}} ``` ## Next StepsThis is the home page content.
{{end}} ``` ### views/partials/nav.html ```html theme={null} ``` ## Asset Files ### assets/embed.go ```go theme={null} package assets import "embed" //go:embed css js var FS embed.FS ``` This embeds CSS and JS files into the binary. ### assets/css/style.css ```css theme={null} * { box-sizing: border-box; } body { font-family: system-ui, sans-serif; line-height: 1.6; margin: 0; padding: 0; } .container { max-width: 800px; margin: 0 auto; padding: 2rem; } nav { background: #333; padding: 1rem; } nav a { color: white; margin-right: 1rem; } ``` ## Adding a New Page 1. Create handler: `handler/contact.go` 2. Create view: `views/pages/contact.html` 3. Add route: `app/web/routes.go` ```go theme={null} // handler/contact.go package handler import "github.com/go-mizu/mizu" func Contact() mizu.Handler { return func(c *mizu.Ctx) error { return c.Render("pages/contact", map[string]any{ "Title": "Contact", }) } } ``` ```html theme={null} {{define "content"}}{{.Date}}
{{.Summary}}
Read more →{{.Post.Date}}
{{.Post.Content}}
This page is served directly from the public folder.
``` ### Run In your terminal, run: ``` go run . ``` Then open your browser and go to [http://localhost:8080](http://localhost:8080). You should see your HTML page with **Hello, Mizu!** centered on the screen. ### How it works The `Static` method uses Go's built-in `http.FileServer` to serve files from a folder. When you visit `/`, Mizu looks inside the `./public` folder and finds the file that matches the requested path. Files like `/index.html`, `/style.css`, and `/script.js` are served automatically without defining routes. ### Try something new Add a new file called `script.js` inside your `public` folder: ```js theme={null} console.log("Hello from Mizu static files"); ``` Link it to your HTML file: ```html theme={null} ``` Reload your browser and open the console to see the message. You now know how to serve HTML, CSS, JavaScript, and images directly from your Go app using Mizu. Next, move on to [Routing Basics](./003-routing-basics) to learn how to create multiple pages and routes. # Routing Basics Source: https://docs.go-mizu.dev/examples/003-routing-basics Learn how to define routes and handlers in Mizu. In this lesson, you will learn how routing works in Mizu. Routing controls what happens when someone visits a specific URL. You can define routes for different paths like `/`, `/about`, or `/api`, and Mizu will call the right handler function automatically. Each route uses a plain Go function that takes a `*mizu.Ctx` and returns an error. This keeps your code simple and easy to follow. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "log" "net/http" "time" "github.com/go-mizu/mizu" ) // Home page handler func home(c *mizu.Ctx) error { return c.Text(http.StatusOK, "Welcome to Mizu!") } // About page handler func about(c *mizu.Ctx) error { return c.Text(http.StatusOK, "This is the About page.") } // Hello page handler with path parameter func hello(c *mizu.Ctx) error { name := c.Param("name") return c.Text(http.StatusOK, "Hello, "+name) } // Time API handler returning JSON func apiTime(c *mizu.Ctx) error { return c.JSON(http.StatusOK, map[string]string{ "time": time.Now().Format(time.RFC3339), }) } func main() { app := mizu.New() // Register routes app.Get("/", home) app.Get("/about", about) app.Get("/hello/{name}", hello) app.Get("/api/time", apiTime) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Open your terminal and run: ``` go run . ``` Then open your browser and visit: * [http://localhost:8080/](http://localhost:8080/) Shows "Welcome to Mizu!" * [http://localhost:8080/about](http://localhost:8080/about) Shows "This is the About page." * [http://localhost:8080/hello/Alice](http://localhost:8080/hello/Alice) Shows "Hello, Alice" * [http://localhost:8080/api/time](http://localhost:8080/api/time) Shows the current time in JSON format. ### How it works Each route is matched to a specific function. For example, `/hello/{name}` includes a placeholder, and Mizu automatically extracts the value so you can read it with `c.Param("name")`. When a request matches a path, Mizu runs the correct handler. Inside your function, you can use `c.Text()` to send text or `c.JSON()` to return structured data. Every handler returns an error so you can add custom error handling later if needed. ### Try something new Add a route that returns a custom status code or HTML content. For example: ```go theme={null} func notFound(c *mizu.Ctx) error { return c.Text(http.StatusNotFound, "Page not found") } ``` Register it like this: ```go theme={null} app.Get("/404", notFound) ``` Then visit [http://localhost:8080/404](http://localhost:8080/404) to test it. Routing is the core of every Mizu app. Once you are comfortable defining routes and handlers, you are ready to build full pages and APIs. Next, continue to [JSON Responses](./004-json-responses) to learn how to send structured data from your routes. # JSON Responses Source: https://docs.go-mizu.dev/examples/004-json-responses Learn how to send JSON data from your routes. In this lesson, you will learn how to send JSON responses using Mizu. JSON is a common format for web apps and APIs to exchange data. Mizu provides a helper method that automatically converts your Go values to JSON and sets the correct headers. You will see how to return structured data from handlers in a clean and simple way. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "log" "net/http" "time" "github.com/go-mizu/mizu" ) // Welcome handler returning JSON func welcome(c *mizu.Ctx) error { data := map[string]string{ "message": "Welcome to Mizu JSON API", } return c.JSON(http.StatusOK, data) } // Current time handler func currentTime(c *mizu.Ctx) error { data := map[string]any{ "time": time.Now().Format(time.RFC3339), "status": "ok", } return c.JSON(http.StatusOK, data) } // User info handler func userInfo(c *mizu.Ctx) error { user := struct { Name string `json:"name"` Email string `json:"email"` }{ Name: "Alice", Email: "alice@example.com", } return c.JSON(http.StatusOK, user) } func main() { app := mizu.New() app.Get("/", welcome) app.Get("/time", currentTime) app.Get("/user", userInfo) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Run the server: ``` go run . ``` Then open these links in your browser or use `curl`: * [http://localhost:8080/](http://localhost:8080/) Returns: `{"message":"Welcome to Mizu JSON API"}` * [http://localhost:8080/time](http://localhost:8080/time) Returns the current server time and a status field. * [http://localhost:8080/user](http://localhost:8080/user) Returns: `{"name":"Alice","email":"alice@example.com"}` ### How it works The `c.JSON(statusCode, data)` method converts any Go value to JSON, sets the `Content-Type: application/json` header, and sends the response. You can return maps, slices, or structs. Mizu uses Go's built-in JSON encoder, and you can use struct tags to define custom field names. If encoding fails, Mizu sends an error response automatically. This makes it easy to build APIs that return structured data without handling JSON manually. ### Try something new Add another route that returns a list of posts: ```go theme={null} func listPosts(c *mizu.Ctx) error { posts := []map[string]any{ {"id": 1, "title": "First Post"}, {"id": 2, "title": "Learning Mizu"}, } return c.JSON(http.StatusOK, posts) } ``` Then register it in `main()`: ```go theme={null} app.Get("/posts", listPosts) ``` Visit [http://localhost:8080/posts](http://localhost:8080/posts) to see the JSON array. Now you can send structured JSON data from any route with just one line of code. Next, continue to [Context and Lifecycle](./005-context-lifecycle) to learn how Mizu manages requests, deadlines, and cleanup. # Context and Lifecycle Source: https://docs.go-mizu.dev/examples/005-context-lifecycle Learn how to handle timeouts and cancellations safely. In this lesson, you will learn how Mizu manages the lifecycle of an HTTP request. Every request in Mizu has its own context, which lets you handle timeouts, cancellations, and background operations safely. This helps you write clean, responsive handlers that stop when the client disconnects or when the server shuts down. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "context" "log" "net/http" "time" "github.com/go-mizu/mizu" ) // Slow handler that respects the request context func slow(c *mizu.Ctx) error { select { case <-time.After(3 * time.Second): return c.Text(http.StatusOK, "Finished after 3 seconds") case <-c.Context().Done(): // Context canceled (client disconnected or timeout reached) return c.Text(499, "Request cancelled") } } // Fast handler that uses a manual timeout func timed(c *mizu.Ctx) error { ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second) defer cancel() select { case <-time.After(1 * time.Second): return c.Text(http.StatusOK, "Completed within timeout") case <-ctx.Done(): return c.Text(http.StatusGatewayTimeout, "Operation timed out") } } func main() { app := mizu.New() app.Get("/slow", slow) app.Get("/timed", timed) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Run the server with: ``` go run . ``` Then visit these URLs in your browser or use `curl`: * [http://localhost:8080/slow](http://localhost:8080/slow) Wait a few seconds to see the message "Finished after 3 seconds". If you stop loading early, the handler detects it and stops. * [http://localhost:8080/timed](http://localhost:8080/timed) Returns quickly with "Completed within timeout". If you increase the delay past 2 seconds, it will show "Operation timed out". ### How it works Each request in Go includes a `context.Context` that ends when the request finishes. Mizu provides `c.Context()` to access it easily. You can use it to cancel work early, set timeouts, or handle cleanup when the request is done. In the `/slow` example, the handler simulates a long operation. If the user closes the browser or the server shuts down, the context is canceled and your code exits early. In `/timed`, you create your own timeout with `context.WithTimeout`, giving full control over how long the handler should run. This approach is important for tasks like database queries, API calls, or background jobs where you should stop safely when the request ends. ### Try something new Change the values inside `time.After()` or `context.WithTimeout()` to see how timeouts behave. You can also print `ctx.Err()` to understand why the context ended. Now you know how Mizu's context system helps manage request lifecycles in a simple, predictable way. Next, continue to [Query, Form, and Path](./006-query-form-path) to learn how to read data from incoming requests. # Query, Form, and Path Parameters Source: https://docs.go-mizu.dev/examples/006-query-form-path Learn how to read query, form, and path data. # Query, Form, and Path In this lesson, you will learn how to read data sent by users through query strings, form submissions, and path parameters. These are the main ways a client can send information to your server. Mizu provides simple helper methods on `Ctx` that make handlers easy to read and maintain. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "log" "net/http" "github.com/go-mizu/mizu" ) // Handle query parameters like /search?q=go func search(c *mizu.Ctx) error { query := c.Query("q") if query == "" { return c.Text(http.StatusBadRequest, "Missing query parameter 'q'") } return c.Text(http.StatusOK, "You searched for: "+query) } // Handle form submissions from HTML forms func submit(c *mizu.Ctx) error { form, err := c.Form() if err != nil { return c.Text(http.StatusBadRequest, "Invalid form data") } name := form.Get("name") email := form.Get("email") if name == "" || email == "" { return c.Text(http.StatusBadRequest, "Both name and email are required") } return c.Text(http.StatusOK, "Form submitted by "+name+" ("+email+")") } // Handle dynamic paths like /hello/alice func greet(c *mizu.Ctx) error { name := c.Param("name") if name == "" { return c.Text(http.StatusBadRequest, "Name is required") } return c.Text(http.StatusOK, "Hello, "+name) } func main() { app := mizu.New() app.Get("/search", search) app.Post("/submit", submit) app.Get("/hello/{name}", greet) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Start your server with: ``` go run . ``` Then test the routes: * Visit [http://localhost:8080/search?q=mizu](http://localhost:8080/search?q=mizu) Response: `You searched for: mizu` * Send a form using curl: ``` curl -X POST -d "name=Alice&email=alice@example.com" http://localhost:8080/submit ``` Response: `Form submitted by Alice (alice@example.com)` * Visit [http://localhost:8080/hello/Bob](http://localhost:8080/hello/Bob) Response: `Hello, Bob` ### How it works Mizu’s `Ctx` helpers wrap parts of Go’s `http.Request` to make input handling simpler. | Method | Reads from | Example | | ----------------- | --------------- | ------------------------------------ | | `c.Query("key")` | Query string | `/search?q=mizu` | | `c.Form()` | Form body | `name=Alice&email=alice@example.com` | | `c.Param("name")` | Path parameters | `/hello/Alice` with `/hello/{name}` | * `Query` reads values from the URL. * `Form` parses key-value pairs from POST data. * `Param` extracts variables from the route path. Each method returns safe, plain strings. If a key is missing, you get an empty string. If parsing fails, `c.Form()` returns an error you can check. These helpers keep your code short and close to Go’s `net/http` design. ### Try something new You can add a small HTML form to test the `/submit` route. Create a file named `form.html`: ```html theme={null}You typed:
Age:
Thank you!
Selected:
Selected:
Country:
Welcome, !
Items:
Total: $
Modal content goes here.
Hello,
Content that expands and collapses smoothly
```
### Morph
Morph DOM elements (useful with HTMX):
```html theme={null}
No todos found. Add one above!
{{ user.email }}
{{ user.email }}
Count: {{ count() }}
Doubled: {{ doubled() }}
` }) export class CounterComponent { // Writable signal count = signal(0) // Computed signal (automatically updates) doubled = computed(() => this.count() * 2) // Effect (runs when dependencies change) constructor() { effect(() => { console.log('Count changed to:', this.count()) }) } increment() { this.count.update(n => n + 1) // Or: this.count.set(this.count() + 1) } decrement() { this.count.update(n => n - 1) } } ``` ### Signals in Services ```ts theme={null} import { Injectable, signal, computed } from '@angular/core' import { HttpClient } from '@angular/common/http' @Injectable({ providedIn: 'root' }) export class TodoService { // State as signals private todosSignal = signal{{ user.email }}
{{ today | date:'fullDate' }}
{{ price | currency:'USD':'symbol':'1.2-2' }}
{{ name | uppercase }}
{{ user | json }}
{{ user.name }}
Posted {{ post.createdAt | timeAgo }}
` }) ``` ## Directives Directives modify element behavior. ### Structural Directives ```ts theme={null} // Custom structural directive import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core' @Directive({ selector: '[appRepeat]', standalone: true }) export class RepeatDirective { constructor( private templateRef: TemplateRefHover over me!
` }) ``` ## HTTP Interceptors Interceptors modify HTTP requests and responses globally. ```ts theme={null} // interceptors/auth.interceptor.ts import { HttpInterceptorFn } from '@angular/common/http' export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = localStorage.getItem('auth_token') if (token) { // Clone and modify request req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) } return next(req) } ``` ```ts theme={null} // interceptors/error.interceptor.ts import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http' import { catchError, throwError } from 'rxjs' import { inject } from '@angular/core' import { Router } from '@angular/router' export const errorInterceptor: HttpInterceptorFn = (req, next) => { const router = inject(Router) return next(req).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { // Redirect to login on 401 router.navigate(['/login']) } return throwError(() => error) }) ) } ``` ```ts theme={null} // app.config.ts export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([authInterceptor, errorInterceptor]) ) ] } ``` ## Lifecycle Hooks Angular components have a rich lifecycle. ```ts theme={null} import { Component, OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy, SimpleChanges } from '@angular/core' @Component({ selector: 'app-lifecycle', template: `Lifecycle example
` }) export class LifecycleComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy { ngOnChanges(changes: SimpleChanges) { // Called before ngOnInit and when input properties change console.log('ngOnChanges', changes) } ngOnInit() { // Called once after first ngOnChanges // Good for: API calls, initialization console.log('ngOnInit') } ngDoCheck() { // Called during every change detection run // Use sparingly - performance impact console.log('ngDoCheck') } ngAfterContentInit() { // Called once after content (ng-content) initialized console.log('ngAfterContentInit') } ngAfterContentChecked() { // Called after content checked during change detection console.log('ngAfterContentChecked') } ngAfterViewInit() { // Called once after view initialized // Good for: DOM manipulation, accessing ViewChild console.log('ngAfterViewInit') } ngAfterViewChecked() { // Called after view checked during change detection console.log('ngAfterViewChecked') } ngOnDestroy() { // Called before component is destroyed // Good for: Cleanup, unsubscribe console.log('ngOnDestroy') } } ``` **Lifecycle order:** 1. `ngOnChanges()` - Input changes 2. `ngOnInit()` - Component initialized 3. `ngDoCheck()` - Change detection 4. `ngAfterContentInit()` - Content initialized 5. `ngAfterContentChecked()` - Content checked 6. `ngAfterViewInit()` - View initialized 7. `ngAfterViewChecked()` - View checked 8. `ngOnDestroy()` - Before destruction ## ViewChild and ContentChild Access child components and DOM elements. ```ts theme={null} import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core' @Component({ selector: 'app-parent', template: `{{ task.description }}
{{ task.priority }}
// ✅ Will be included
import logo from './logo.png'
//
Are you sure you want to delete this user?
| {{ .Name }} |
| Title | Status | Priority | Actions |
|---|---|---|---|
| No tasks found | |||
```
## Manual Tag Generation
### Script Tag
```go theme={null}
scriptTag := manifest.ScriptTag("src/main.tsx")
// Returns:
```
### CSS Tags
```go theme={null}
cssTags := manifest.CSSTags("src/main.tsx")
// Returns:
```
### Preload Tags
```go theme={null}
preloadTags := manifest.PreloadTags("src/main.tsx")
// Returns:
```
### Entry Tags (All in One)
```go theme={null}
allTags := manifest.EntryTags("src/main.tsx")
// Returns: CSS + preloads + script in correct order
```
## Vite Manifest Format
Vite creates `.vite/manifest.json`:
```json theme={null}
{
"src/main.tsx": {
"file": "assets/main.abc123.js",
"src": "src/main.tsx",
"isEntry": true,
"css": ["assets/main.def456.css"],
"imports": ["_vendor.xyz789.js"]
},
"_vendor.xyz789.js": {
"file": "assets/vendor.xyz789.js"
}
}
```
## Webpack Manifest Format
Webpack creates a simpler manifest:
```json theme={null}
{
"main.js": "assets/main.abc123.js",
"main.css": "assets/main.def456.css",
"logo.png": "assets/logo.abc123.png"
}
```
Both formats work with Mizu's manifest loader.
## Benefits
### 1. Correct Asset Loading
Always load the correct hashed file:
```html theme={null}
{{ vite_entry "src/main.tsx" }}
```
### 2. Module Preloading
Improve performance by preloading dependencies:
```html theme={null}
```
Browser loads dependencies in parallel.
### 3. CSS Extraction
Automatically include CSS:
```html theme={null}
```
## Next Steps
Email: {user.email}
ID: {user.id}
Built at: {buildTime}
Count: {count}
{data.message}
{/* Client component nested in server component */}
}
```
### Option 2: Use `next-image-export-optimizer`
This package optimizes images at build time:
```bash theme={null}
npm install next-image-export-optimizer
```
```js theme={null}
// next.config.mjs
const nextConfig = {
output: 'export',
images: {
loader: 'custom',
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
},
env: {
nextImageExportOptimizer_imageFolderPath: "public/images",
nextImageExportOptimizer_exportFolderPath: "build",
nextImageExportOptimizer_quality: "75",
},
}
```
```tsx theme={null}
import ExportedImage from 'next-image-export-optimizer'
export default function Logo() {
return (
{new Date().toString()}
} // ✅ Good - use client component for dynamic content 'use client' export default function Page() { const [time, setTime] = useState(new Date()) useEffect(() => { const timer = setInterval(() => setTime(new Date()), 1000) return () => clearInterval(timer) }, []) return{time.toString()}
} ``` *** ### Images Not Loading **Error:** Images show broken icon **Cause:** Next.js Image component requires server **Solution:** Use one of the image optimization alternatives mentioned above. *** ### Can't Find Module **Error:** ``` Module not found: Can't resolve '@/components/Header' ``` **Cause:** Path alias not configured **Solution:** Check `tsconfig.json`: ```json theme={null} { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./app/*"] } } } ``` ## Real-World Example: Blog with Comments Here's a complete example showing Server Components, Client Components, and API integration: ### Backend ```go theme={null} // app/server/routes.go func setupRoutes(app *mizu.App) { app.Get("/api/posts", handlePosts) app.Get("/api/posts/{id}", getPost) app.Get("/api/posts/{id}/comments", getComments) app.Post("/api/posts/{id}/comments", createComment) } func handlePosts(c *mizu.Ctx) error { posts := []map[string]any{ {"id": 1, "title": "First Post", "slug": "first-post"}, {"id": 2, "title": "Second Post", "slug": "second-post"}, } return c.JSON(200, posts) } func getPost(c *mizu.Ctx) error { id := c.Param("id") post := map[string]any{ "id": id, "title": "Post " + id, "content": "This is the content of post " + id, } return c.JSON(200, post) } func getComments(c *mizu.Ctx) error { // Return comments for post comments := []map[string]any{ {"id": 1, "author": "Alice", "text": "Great post!"}, {"id": 2, "author": "Bob", "text": "Thanks for sharing"}, } return c.JSON(200, comments) } ``` ### Frontend ```tsx theme={null} // app/blog/[slug]/page.tsx import Comments from './Comments' export default function BlogPost({ params }: { params: { slug: string } }) { // Server Component - runs at build time return (This is a blog post about {params.slug}
{/* Client Component for interactivity */}Email: {{ user.email }}
Role: {{ user.role }}
Slug: {{ slug }}
Count: {{ count }}
{{ user.email }}
{{ user.role }}Admins: {{ userStore.adminUsers.length }}
Count: {{ count }}
Static content
{{ new Date().toString() }}
{content}
{user.email}
Count: {this.state.count}
{name}: {count}
setName(e.target.value)} />Count: {state.count}
Count: {count}
Count: {count}
Doubled: {doubled}
Total: {users.value.length}
No todos found
} return (Count: {count}
setText(e.target.value)} />Count: {state.count}
dispatch({ type: 'setText', payload: e.target.value })} />Filtered: {filteredItems.length}
Total: {total}
Current: {count}
Previous: {prevCount.current}
{this.state.error?.message}
Something went wrong:
{error.message}
Content
{user.name}
No tasks found
} return ({loaderData.user.email}
{user.email}
{user.role}{loaderData.user.email}
{loaderData.user.role}{user.email}
Loading...
{:else if error}Error: {error}
{:else}{count}
``` **How it works:** Svelte's compiler analyzes your code and inserts update calls wherever variables are reassigned. ### Reactive Declarations (\$:) Use `$:` to create reactive statements that automatically re-run when dependencies change. ```svelte theme={null}Count: {count}
Doubled: {doubled}
``` ### Reactive Dependencies Svelte automatically tracks dependencies: ```svelte theme={null}Full name: {fullName}
Initials: {initials}
``` ### Array and Object Reactivity **Important:** Assignments trigger updates, but mutating methods (push, pop, etc.) don't. ```svelte theme={null}Numbers: {numbers.join(', ')}
``` ## Component Communication ### Props Pass data from parent to child using `export let`. ```svelte theme={null}{user.email}
{/if}Count: {count}
This is the card body
Count: {$count}
``` ### Readable Stores For read-only values (e.g., time, mouse position): ```ts theme={null} // stores/time.ts import { readable } from 'svelte/store' export const time = readable(new Date(), (set) => { const interval = setInterval(() => { set(new Date()) }, 1000) // Cleanup function return () => clearInterval(interval) }) ``` ```svelte theme={null}Current time: {$time.toLocaleTimeString()}
``` ### Derived Stores Create stores derived from other stores: ```ts theme={null} // stores/user.ts import { writable, derived } from 'svelte/store' interface User { id: number name: string email: string } export const users = writableTotal users: {$userCount}
Names: {$userNames.join(', ')}
``` ### Custom Stores Create stores with custom methods: ```ts theme={null} // stores/tasks.ts import { writable } from 'svelte/store' interface Task { id: number title: string done: boolean } function createTaskStore() { const { subscribe, set, update } = writableLoading user...
{:else if user}{user.email}
{:else}User not found
{/if}{count}
``` **Lifecycle order:** 1. Component created 2. `onMount()` - After initial render 3. `beforeUpdate()` - Before each update 4. `afterUpdate()` - After each update 5. `onDestroy()` - Before removal ## Bindings Svelte provides powerful two-way bindings. ### Input Bindings ```svelte theme={null} ``` ### Element Bindings (bind:this) Get a reference to a DOM element: ```svelte theme={null} ``` ### Component Bindings Bind to component props: ```svelte theme={null} ``` ```svelte theme={null}Text: {text}
``` ### Dimensions and Scroll ```svelte theme={null}Scrolled: {scrollY}px
``` ## Logic Blocks ### `{#if}` blocks ```svelte theme={null} {#if count === 0}Count is zero
{:else if count < 5}Count is less than 5
{:else}Count is {count}
{/if} ``` ### `{#each}` blocks ```svelte theme={null} {#each items as item (item.id)}{item.name}
{/each} {#each items as item, index (item.id)}{index + 1}. {item.name}
{/each} {#each items as item}{item.name}
{:else}No items found
{/each} ``` ### `{#await}` blocks Handle promises declaratively: ```svelte theme={null} {#await promise}Loading...
{:then user}{user.name}
{:catch error}{error.message}
{/await} {#await promise then user}{user.name}
{/await} ``` ### `{#key}` blocks Re-render when value changes: ```svelte theme={null} {#key value}Scroll: {scrollY}px, Width: {innerWidth}px
``` ### `{$progress.toFixed(0)}%
``` ### Spring Physics-based animation: ```svelte theme={null}Styled text
Text
Text
``` ## Vite Configuration ### `frontend/vite.config.ts` ```ts theme={null} import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' import path from 'path' export default defineConfig({ plugins: [svelte()], resolve: { alias: { '@': path.resolve(__dirname, './src'), '@components': path.resolve(__dirname, './src/components'), '@pages': path.resolve(__dirname, './src/pages'), '@stores': path.resolve(__dirname, './src/stores'), }, }, server: { port: 5173, strictPort: true, hmr: { clientPort: 3000, // Mizu's port for HMR }, }, build: { outDir: '../dist', emptyOutDir: true, sourcemap: false, rollupOptions: { output: { manualChunks: { 'svelte-vendor': ['svelte'], }, }, }, }, }) ``` ### `frontend/svelte.config.js` ```js theme={null} import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { preprocess: vitePreprocess(), compilerOptions: { // Enable CSS hash for production cssHash: ({ hash, css }) => `svelte-${hash(css)}` } } ``` ## Performance Optimization ### Immutable Data Tell Svelte your data is immutable for performance: ```svelte theme={null}Content
{task.description}
{task.priority}{count}
{$count}
``` ### TypeScript Errors with Stores **Problem**: Type errors when using stores. **Solution**: Properly type your stores: ```ts theme={null} import { writable, type Writable } from 'svelte/store' interface User { name: string email: string } export const user: WritableLoading stats...
{:then stats}Posts: {stats.postCount}
Views: {stats.viewCount}
Loading posts...
{:then posts}Current page: {$page.url.pathname}
``` ## Forms and Actions In static mode, SvelteKit form actions don't work. Use client-side forms instead. ### Client-Side Form Handling ```svelte theme={null} {#if success}Message sent successfully!
{/if} {#if error}{error}
{/if} ``` ## Error Handling Handle errors with `+error.svelte` pages. ### Error Page ```svelte theme={null}Page not found
Go home {:else}Something went wrong
{/if}{post.excerpt}
Count: {{ count }}
Count: {{ count }}
Count: {{ count }}
Name: {{ name }}
``` ### `reactive()` - Reactive Objects Use `reactive()` for objects and arrays. No `.value` needed. ```vue theme={null}{{ user.name }}, {{ user.age }}
{{ fullName }}
{{ user.email }}
Name: {{ user.name }}
Email: {{ user.email }}
{{ text }}
``` ### Multiple v-models ```vue theme={null}Hello Vue!
User created successfully!
{{ error }}
``` ### Form Validation with VeeValidate ```bash theme={null} cd frontend npm install vee-validate yup ``` ```vue theme={null} ``` ## Styling Options ### Scoped Styles Vue's scoped styles are built-in: ```vue theme={null} ``` ### CSS Modules ```vue theme={null}Multiple classes
``` ### Tailwind CSS Install Tailwind: ```bash theme={null} cd frontend npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` Configure `tailwind.config.js`: ```js theme={null} export default { content: [ "./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}", ], theme: { extend: { colors: { primary: '#42b983', }, }, }, plugins: [], } ``` Add to `src/styles/index.css`: ```css theme={null} @tailwind base; @tailwind components; @tailwind utilities; @layer components { .btn-primary { @apply bg-primary text-white px-4 py-2 rounded hover:bg-green-600 transition; } } ``` Use in components: ```vue theme={null}{{ initialMessage }}
{{ user.name }}
{{ user.email }}
{{ error.message }}
{{ task.description }}
{{ task.priority }}Hello from Mizu!
") } ``` For templates, use Go's `html/template`: ```go theme={null} var tmpl = template.Must(template.New("page").Parse(`{{.Message}}
`)) func handler(c *mizu.Ctx) error { c.Header().Set("Content-Type", "text/html; charset=utf-8") return tmpl.Execute(c.Writer(), map[string]string{ "Title": "Welcome", "Message": "Hello!", }) } ``` ## Setting status codes Each response method takes a status code as the first argument: ```go theme={null} return c.JSON(201, createdUser) // 201 Created return c.Text(400, "bad input") // 400 Bad Request return c.JSON(500, errorBody) // 500 Internal Server Error ``` If you pass `0`, Mizu uses the previously set status (default 200): ```go theme={null} c.Status(202) return c.Text(0, "Accepted") // Uses status 202 ``` Check the current status: ```go theme={null} code := c.StatusCode() ``` ## Working with headers Set headers before writing the body: ```go theme={null} func handler(c *mizu.Ctx) error { // Set a header c.Header().Set("Cache-Control", "max-age=3600") // Set only if not already present c.HeaderIfNone("X-Request-Id", "abc123") return c.JSON(200, data) } ``` Once you write the body (via `c.JSON()`, `c.Text()`, etc.), headers are sent and can't be changed. ## Redirects Send the client to a different URL: ```go theme={null} func handler(c *mizu.Ctx) error { return c.Redirect(302, "/login") } ``` | Code | Meaning | When to use | | ---- | ------------------ | ------------------------------------------ | | 301 | Moved Permanently | URL changed forever | | 302 | Found | Temporary redirect (default if you pass 0) | | 303 | See Other | Redirect after POST | | 307 | Temporary Redirect | Preserve method | | 308 | Permanent Redirect | Preserve method | ## Empty responses Return no body with status 204: ```go theme={null} func deleteUser(c *mizu.Ctx) error { // ... delete the user return c.NoContent() // 204 No Content } ``` ## Serving files Serve a file from disk: ```go theme={null} func handler(c *mizu.Ctx) error { // File(statusCode, filePath) return c.File(200, "./public/logo.png") } ``` Force the browser to download (adds Content-Disposition header): ```go theme={null} func handler(c *mizu.Ctx) error { // Download(statusCode, filePath, downloadName) return c.Download(200, "./reports/data.csv", "report-2024.csv") } ``` Both methods: * Auto-detect Content-Type from the file extension * Support range requests (for video seeking, resumable downloads) * Handle If-Modified-Since caching ## Streaming responses Send data gradually as it becomes available: ```go theme={null} func handler(c *mizu.Ctx) error { return c.Stream(func(w io.Writer) error { for i := 0; i < 10; i++ { fmt.Fprintf(w, "chunk %d\n", i) time.Sleep(100 * time.Millisecond) } return nil }) } ``` ## Server-Sent Events (SSE) Send real-time updates to the client: ```go theme={null} func events(c *mizu.Ctx) error { ch := make(chan any) // Send updates from a goroutine go func() { defer close(ch) // Closing the channel ends the stream for i := 0; i < 10; i++ { ch <- map[string]int{"count": i} time.Sleep(time.Second) } }() return c.SSE(ch) } ``` SSE sets these headers automatically: * `Content-Type: text/event-stream` * `Cache-Control: no-cache` * `Connection: keep-alive` Client-side JavaScript: ```javascript theme={null} const events = new EventSource('/events'); events.onmessage = (e) => { const data = JSON.parse(e.data); console.log('Received:', data); }; ``` ## Raw bytes Send arbitrary bytes with a custom content type: ```go theme={null} func handler(c *mizu.Ctx) error { data := []byte{0x00, 0x01, 0x02} return c.Bytes(200, data, "application/octet-stream") } ``` ## Method reference | Method | Signature | Purpose | | ----------- | --------------------------------------- | ------------------ | | `Text` | `(code int, s string) error` | Plain text | | `JSON` | `(code int, v any) error` | JSON data | | `HTML` | `(code int, s string) error` | HTML content | | `File` | `(code int, path string) error` | Serve file | | `Download` | `(code int, path, name string) error` | Force download | | `Bytes` | `(code int, b []byte, ct string) error` | Raw bytes | | `Stream` | `(fn func(io.Writer) error) error` | Streaming | | `SSE` | `(ch <-chan any) error` | Server-Sent Events | | `Redirect` | `(code int, url string) error` | Redirect | | `NoContent` | `() error` | 204 No Content | ## Next stepsBuilt with Mizu + React
No tasks yet. Add one above!
)}{{.Excerpt}}
Read more →No posts yet. Create your first post!
{{end}} {{end}} ``` Create `templates/post.html`: ```html theme={null} {{define "content"}}Missing security token. Please try again.
`) } return c.HTML(403, `Invalid security token. Please refresh and try again.
`) }, })) ``` ### Multiple Token Sources ```go theme={null} // Accept token from either header or form app.Use(csrf.New(csrf.Options{ Secret: secret, TokenLookup: "form:_csrf", // Primary: form field })) // Alternative: Check header if form field missing // (Requires custom implementation) ``` ### Development vs Production ```go theme={null} func setupCSRF(app *mizu.App, isDev bool) { secret := []byte(os.Getenv("CSRF_SECRET")) if isDev { app.Use(csrf.ProtectDev(secret)) } else { app.Use(csrf.Protect(secret)) } } ``` ## API Reference ### Functions ```go theme={null} // New creates CSRF middleware with options func New(opts Options) mizu.Middleware // Protect creates middleware with secure cookies (production) func Protect(secret []byte) mizu.Middleware // ProtectDev creates middleware with insecure cookies (development) func ProtectDev(secret []byte) mizu.Middleware // GenerateSecret generates a secure random secret func GenerateSecret() []byte ``` ### Token Functions ```go theme={null} // Token extracts CSRF token from context func Token(c *mizu.Ctx) string // TemplateField returns HTML hidden input field func TemplateField(c *mizu.Ctx) string // TokenExpiry returns token expiration time func TokenExpiry(opts Options) time.Time ``` ### Error Types ```go theme={null} var ( ErrTokenMissing // CSRF token not found in request ErrTokenInvalid // CSRF token validation failed ) ``` ## How It Works 1. **Token Generation**: On GET requests, a token is generated and stored in a cookie 2. **Token Validation**: On POST/PUT/DELETE requests, the token must be included 3. **Double Submit**: The cookie token must match the request token 4. **HMAC Signature**: Tokens are signed to prevent tampering ``` GET /form ← Sets _csrf cookie with token ← Provides token for form POST /submit → Sends _csrf cookie (automatic) → Sends token in form/header ← Server validates both match ``` ## Technical Details ### Token Generation The middleware generates cryptographically secure tokens using the following process: 1. **Random Bytes**: Generates random bytes of specified length (default 32) using `crypto/rand` 2. **HMAC Signature**: Creates an HMAC-SHA256 signature of the random bytes using the secret key 3. **Encoding**: Encodes both the token and signature using base64 URL encoding 4. **Format**: Combines them as `token.signature` for tamper detection ```go theme={null} // Token format: base64(random_bytes).base64(hmac_sha256(random_bytes)) token := generateToken(32, secret) // Example: "a1b2c3d4...xyz.9f8e7d6c..." ``` ### Token Validation Validation follows a multi-step process to ensure security: 1. **Constant-Time Comparison**: Uses `subtle.ConstantTimeCompare` to prevent timing attacks 2. **Cookie-Request Match**: Verifies the cookie token exactly matches the request token 3. **Signature Verification**: Decodes and validates the HMAC signature 4. **HMAC Equality**: Uses `hmac.Equal` for secure signature comparison ### Safe vs Unsafe Methods The middleware distinguishes between HTTP methods: * **Safe Methods** (GET, HEAD, OPTIONS, TRACE): Generate or reuse tokens, no validation required * **Unsafe Methods** (POST, PUT, DELETE, PATCH): Require valid token in both cookie and request ### Context Storage Tokens are stored in the request context using a private `contextKey` type to prevent collisions with other middleware or application code. This allows retrieval via the `Token()` function throughout the request lifecycle. ### Path Skipping The `SkipPaths` option uses a map for O(1) lookup performance, allowing specific routes (like webhooks or public APIs) to bypass CSRF validation entirely. ## Security Considerations 1. **Secret Key** - Use a strong, random 32-byte secret 2. **Cookie Settings** - Use `Secure`, `HttpOnly`, and `SameSite` in production 3. **Token Rotation** - Tokens are per-session, not per-request 4. **HTTPS Required** - Always use HTTPS in production ### Recommended Production Settings ```go theme={null} app.Use(csrf.New(csrf.Options{ Secret: []byte(os.Getenv("CSRF_SECRET")), CookieSecure: true, CookieHTTPOnly: true, SameSite: http.SameSiteStrictMode, CookieMaxAge: 3600, // 1 hour })) ``` ## Best Practices * Always include CSRF protection for forms * Use secure cookies in production * Skip CSRF for API endpoints using Bearer auth * Regenerate token after login/logout * Set appropriate cookie expiration ## Testing The CSRF middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ------------------------------- | ---------------------------------------------- | --------------------------------------------------------- | | **Basic Functionality** | | | | Sets cookie on GET | GET request to protected endpoint | CSRF cookie is set with generated token | | Rejects POST without token | POST request without CSRF token | Returns 403 Forbidden | | Accepts POST with valid token | POST with matching cookie and header token | Request succeeds with 200 OK | | Rejects POST with invalid token | POST with mismatched tokens | Returns 403 Forbidden | | **Token Lookup Sources** | | | | Form token lookup | POST with token in form field | Token extracted from form and validated | | Query token lookup | POST with token in query parameter | Token extracted from query and validated | | Header token lookup | POST with token in header (default) | Token extracted from header and validated | | **Path Skipping** | | | | Skips listed path | POST to path in SkipPaths | Request bypasses CSRF validation | | Protects non-listed path | POST to path not in SkipPaths | CSRF validation required | | **Error Handling** | | | | Custom error handler | POST without token with ErrorHandler set | Custom error handler called with error | | Default error handler | POST without token, no ErrorHandler | Returns 403 with error text | | **Helper Functions** | | | | TemplateField generation | Calling TemplateField() with context | Returns HTML hidden input with token | | Protect middleware | Creating middleware with Protect() | Secure cookies enabled by default | | ProtectDev middleware | Creating middleware with ProtectDev() | Insecure cookies for development | | GenerateSecret uniqueness | Generating multiple secrets | Each secret is unique and 32 bytes | | **Validation Logic** | | | | Valid token validation | Validating matching tokens with correct secret | Returns true | | Mismatched tokens | Validating different tokens | Returns false | | Invalid signature | Validating token with wrong secret | Returns false | | Malformed token | Validating token without signature separator | Returns false | | **Token Generation** | | | | Token format | Generated token structure | Contains random bytes and HMAC signature separated by "." | | Token uniqueness | Generating 100 tokens | All tokens are unique | | **Configuration Validation** | | | | Panics without secret | Creating middleware with empty secret | Panics with error message | | Panics with invalid TokenLookup | Creating middleware with malformed TokenLookup | Panics with error message | ## Related Middlewares * [secure](/middlewares/secure) - HTTPS enforcement * [helmet](/middlewares/helmet) - Security headers # CSRF2 Source: https://docs.go-mizu.dev/middlewares/csrf2 Enhanced CSRF protection with double submit cookie pattern. ## Overview The `csrf2` middleware provides enhanced CSRF protection using the double submit cookie pattern, where a token is stored in both a cookie and must be submitted in a header or form field. Use it when you need: * Stateless CSRF protection * Cookie-based token validation * Enhanced security for SPAs ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/csrf2" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(csrf2.New(csrf2.Options{})) app.Post("/submit", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{"status": "ok"}) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------------- | ----------------------- | ---------------------------- | -------------------- | | `CookieName` | `string` | `"csrf_token"` | Cookie name | | `HeaderName` | `string` | `"X-CSRF-Token"` | Header name | | `FormField` | `string` | `"csrf_token"` | Form field name | | `CookiePath` | `string` | `"/"` | Cookie path | | `CookieSecure` | `bool` | `false` | HTTPS only | | `CookieSameSite` | `http.SameSite` | `Lax` | SameSite policy | | `IgnoreMethods` | `[]string` | `["GET", "HEAD", "OPTIONS"]` | Methods to skip | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Custom error handler | ## Examples ### Basic Usage ```go theme={null} app.Use(csrf2.New(csrf2.Options{})) ``` ### Secure Configuration ```go theme={null} app.Use(csrf2.New(csrf2.Options{ CookieName: "_csrf", HeaderName: "X-CSRF-Token", CookieSecure: true, CookieSameSite: http.SameSiteStrictMode, })) ``` ### Custom Error Handler ```go theme={null} app.Use(csrf2.New(csrf2.Options{ ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(403, map[string]string{ "error": "Invalid CSRF token", }) }, })) ``` ### Get Token for Forms ```go theme={null} app.Get("/form", func(c *mizu.Ctx) error { token := csrf2.Token(c) return c.HTML(200, ` `) }) ``` ## How It Works 1. Middleware sets CSRF token in cookie 2. Client reads cookie and includes token in header/form 3. On state-changing requests, token is validated 4. If tokens match, request proceeds 5. If not, returns 403 Forbidden ## API Reference ### Functions ```go theme={null} // New creates CSRF2 middleware func New(opts Options) mizu.Middleware // Token returns CSRF token from context func Token(c *mizu.Ctx) string ``` ## Client Implementation ### JavaScript (SPA) ```javascript theme={null} // Read token from cookie function getCsrfToken() { return document.cookie .split('; ') .find(row => row.startsWith('csrf_token=')) ?.split('=')[1]; } // Include in requests fetch('/api/submit', { method: 'POST', headers: { 'X-CSRF-Token': getCsrfToken(), 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` ## Technical Details ### Token Generation The middleware generates cryptographically secure tokens using the following process: 1. **Random Bytes**: Generates random bytes using `crypto/rand` 2. **Timestamp**: Appends current Unix timestamp for rotation support 3. **Signature**: Signs the token with SHA-256 using the provided secret 4. **Encoding**: Base64 URL-safe encoding for transport ### Token Validation Token validation uses constant-time comparison to prevent timing attacks: 1. **Decode**: Both cookie and submitted tokens are base64-decoded 2. **Length Check**: Validates minimum token length (8 bytes) 3. **Constant-Time Compare**: Uses XOR-based comparison across all bytes 4. **Result**: Returns validation result without early exit ### Cookie Storage Tokens are stored in HTTP cookies with configurable security options: * **HttpOnly**: Default true to prevent JavaScript access * **Secure**: Configurable for HTTPS enforcement * **SameSite**: Default Lax mode for CSRF protection * **MaxAge**: Default 24 hours (86400 seconds) * **Path**: Default "/" for site-wide availability ### Token Retrieval The middleware checks for tokens in the following order: 1. **Header**: Checks configured header name (default: `X-CSRF-Token`) 2. **Form**: Parses form data for configured field name (default: `_csrf`) 3. **Query**: Checks URL query parameters as fallback ### Origin Validation When enabled, the middleware validates request origin: 1. **Origin Header**: Primary validation source 2. **Referer Fallback**: Used if Origin header is missing 3. **Allowed Origins**: Compares against configured whitelist 4. **Host Match**: Falls back to request host if no whitelist specified ### Advanced Features * **Token Masking**: XOR-based masking for additional security * **Fingerprinting**: SHA-256 hash of User-Agent and Accept headers * **Form Input**: HTML helper for hidden form fields * **Meta Tag**: HTML meta tag helper for SPAs ## Best Practices * Always use HTTPS in production * Use SameSite Strict for sensitive applications * Include token in all state-changing requests * Regenerate tokens on authentication changes ## Testing ### Test Coverage | Test Case | Description | Expected Behavior | | ------------------------- | ------------------------------- | --------------------------------------------------------------- | | `TestNew` | Basic middleware initialization | Creates middleware and sets CSRF cookie on GET request | | `TestGetToken` | Token retrieval from context | Token is stored in context and retrievable via `GetToken()` | | `TestValidateTokenHeader` | Token validation via header | POST request with valid token in header succeeds (200 OK) | | `TestValidateTokenForm` | Token validation via form field | POST request with valid token in form field succeeds (200 OK) | | `TestMissingToken` | Request without CSRF token | POST request without token returns 403 Forbidden | | `TestInvalidToken` | Request with invalid token | POST request with invalid token returns 403 Forbidden | | `TestSkipPaths` | Path exclusion from validation | Configured paths skip CSRF validation | | `TestSkipMethods` | HTTP method exclusion | Safe methods (GET, HEAD, OPTIONS) skip validation | | `TestCustomErrorHandler` | Custom error handling | Custom error handler returns JSON response on failure | | `TestCustomCookieName` | Custom cookie configuration | CSRF cookie uses configured custom name | | `TestCustomHeaderName` | Custom header configuration | Token validation accepts configured custom header name | | `TestTokenHandler` | Token endpoint handler | Token handler returns JSON with current CSRF token | | `TestFormInput` | HTML form input helper | Generates hidden input field with token value | | `TestMetaTag` | HTML meta tag helper | Generates meta tag with csrf-token name and value | | `TestMaskUnmask` | Token masking/unmasking | Masked token differs from original, unmask restores original | | `TestFingerprint` | Request fingerprinting | Same headers produce same fingerprint, different headers differ | ## Related Middlewares * [csrf](/middlewares/csrf) - Basic CSRF protection * [session](/middlewares/session) - Session management * [secure](/middlewares/secure) - Security settings # Embed Source: https://docs.go-mizu.dev/middlewares/embed Embedded filesystem middleware for serving bundled assets. ## Overview The `embed` middleware provides helpers for serving files from Go's embedded filesystem (embed.FS), enabling single-binary deployments. Use it when you need: * Single binary deployment * Bundled static assets * Embedded templates ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/embed" ``` ## Quick Start ```go theme={null} //go:embed static/* var staticFS embed.FS app := mizu.New() app.Use(embed.Static(staticFS, "static")) ``` ## Examples ### Serve Static Files ```go theme={null} //go:embed static/* var staticFS embed.FS app.Use(embed.Static(staticFS, "static")) // Files at static/css/app.css available at /css/app.css ``` ### With Prefix ```go theme={null} //go:embed assets/* var assetsFS embed.FS app.Use(embed.WithOptions(embed.Options{ FS: assetsFS, Root: "assets", Prefix: "/assets", })) // /assets/js/app.js -> assets/js/app.js ``` ### Multiple Embeds ```go theme={null} //go:embed static/* var staticFS embed.FS //go:embed templates/* var templatesFS embed.FS // Serve static files app.Use(embed.Static(staticFS, "static")) // Load templates tmpl := embed.MustParseTemplates(templatesFS, "templates/*.html") ``` ### SPA with Embed ```go theme={null} //go:embed dist/* var distFS embed.FS app.Use(embed.SPA(distFS, "dist")) ``` ## API Reference ### Functions ```go theme={null} // Static serves embedded static files func Static(fs embed.FS, root string) mizu.Middleware // SPA serves embedded SPA func SPA(fs embed.FS, root string) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // Helper functions func MustParseTemplates(fs embed.FS, pattern string) *template.Template func ReadFile(fs embed.FS, name string) ([]byte, error) ``` ## Build Considerations ```go theme={null} // Embed entire directory //go:embed static/* // Embed specific files //go:embed favicon.ico robots.txt // Embed with pattern //go:embed templates/*.html templates/**/*.html ``` ## Technical Details ### Implementation Overview The embed middleware provides a bridge between Go's `embed.FS` and Mizu's HTTP serving capabilities. It wraps the standard library's `http.FileServer` with additional features like custom root directories, caching headers, and SPA routing. ### Core Components **Filesystem Handling** * Uses `fs.FS` interface for maximum flexibility * Supports `fs.Sub` to create subdirectory filesystems * Falls back to original filesystem if subdirectory doesn't exist * Implements proper path cleaning and normalization **File Serving Strategy** 1. Cleans and normalizes the request path 2. Attempts to open the requested file 3. Falls back to index file for directory requests 4. Delegates to `NotFoundHandler` or next middleware if file not found 5. Uses `http.FileServer` for actual serving (handles range requests, content-type, etc.) **Cache Control** * Optional `MaxAge` setting for Cache-Control headers * Custom `itoa` function to avoid allocations for header values * Headers set before delegating to file server **SPA Support** * Custom `NotFoundHandler` that serves index.html for missing routes * Preserves normal file serving for actual assets * Uses `http.ServeContent` for proper handling of conditional requests ### Handler vs Middleware The package provides both middleware and handler functions: * **Middleware** (`New`, `WithOptions`, `Static`, etc.): Falls through to next handler if file not found * **Handler** (`Handler`, `HandlerWithOptions`): Directly serves files without fallthrough ## Best Practices * Use for production deployments * Keep embedded files small * Use content hashing for cache busting * Test embedded files in CI ## Testing The embed middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ----------------------------------------- | ------------------------------------- | --------------------------------------------------------------- | | `TestNew` | Basic middleware creation and serving | Serves root index.html and nested files successfully | | `TestWithOptions_Root` | Serving from a subdirectory | Files from subdirectory accessible at root URL path | | `TestWithOptions_MaxAge` | Cache control headers | Cache-Control header set to specified max-age value | | `TestWithOptions_NotFoundHandler` | Custom not found handling | Custom handler invoked for missing files with JSON response | | `TestHandler` | Direct handler (non-middleware) | Serves files without middleware chain | | `TestStatic` | Static helper function | Serves files from specified subdirectory | | `TestWithCaching` | Caching helper function | Cache-Control header set correctly | | `TestIndexFile` | Index file serving | Root and subdirectory index files served automatically | | `TestFallthrough` | Middleware fallthrough | Non-existent files fall through to next handler | | `TestSPA` | Single-page application mode | Existing files served normally, unknown routes serve index.html | | `TestSPA_DefaultIndex` | SPA with default index | Empty index parameter defaults to "index.html" | | `TestHandlerWithOptions` | Handler with options | Options applied correctly in handler mode | | `TestHandlerWithOptions_Root` | Handler with custom root | Files served from specified root directory | | `TestWithOptions_InvalidRoot` | Invalid root directory handling | Falls back to original filesystem when root doesn't exist | | `TestWithOptions_PathWithoutLeadingSlash` | Path normalization | Paths without leading slash handled correctly | | `TestItoa` | Integer to string conversion | Custom itoa function converts integers correctly | ## Related Middlewares * [static](/middlewares/static) - File system static files * [spa](/middlewares/spa) - SPA support * [favicon](/middlewares/favicon) - Favicon serving # Envelope Source: https://docs.go-mizu.dev/middlewares/envelope Response envelope middleware for consistent API response structure. ## Overview The `envelope` middleware wraps all responses in a consistent structure, adding metadata like status, pagination, and timestamps. Use it when you need: * Consistent API responses * Response metadata * Standardized error format ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/envelope" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(envelope.New()) app.Get("/users", func(c *mizu.Ctx) error { return c.JSON(200, users) }) // Response: {"success": true, "data": [...users]} ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------------ | -------- | ----------- | ------------------ | | `SuccessKey` | `string` | `"success"` | Success field name | | `DataKey` | `string` | `"data"` | Data field name | | `ErrorKey` | `string` | `"error"` | Error field name | | `IncludeTimestamp` | `bool` | `false` | Add timestamp | | `IncludeRequestID` | `bool` | `false` | Add request ID | ## Examples ### Default Envelope ```go theme={null} app.Use(envelope.New()) // Success: {"success": true, "data": {...}} // Error: {"success": false, "error": "message"} ``` ### With Metadata ```go theme={null} app.Use(envelope.WithOptions(envelope.Options{ IncludeTimestamp: true, IncludeRequestID: true, })) // {"success": true, "data": {...}, "timestamp": "...", "request_id": "..."} ``` ### Custom Structure ```go theme={null} app.Use(envelope.WithOptions(envelope.Options{ SuccessKey: "ok", DataKey: "result", ErrorKey: "message", })) // {"ok": true, "result": {...}} ``` ### With Pagination ```go theme={null} app.Use(envelope.New()) app.Get("/users", func(c *mizu.Ctx) error { envelope.SetMeta(c, map[string]any{ "page": 1, "per_page": 20, "total": 100, "total_pages": 5, }) return c.JSON(200, users) }) // {"success": true, "data": [...], "meta": {"page": 1, ...}} ``` ## API Reference ### Functions ```go theme={null} // New creates envelope middleware func New() mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // SetMeta adds metadata to response func SetMeta(c *mizu.Ctx, meta map[string]any) ``` ## Response Format ### Success Response ```json theme={null} { "success": true, "data": { ... }, "meta": { ... } } ``` ### Error Response ```json theme={null} { "success": false, "error": "Error message", "code": "ERROR_CODE" } ``` ## Technical Details ### Implementation Architecture The envelope middleware uses a custom response writer (`envelopeWriter`) to intercept and capture the original response before wrapping it in the envelope structure. #### Response Writer The middleware captures responses through a buffer-based writer: * Intercepts `WriteHeader()` calls to capture HTTP status codes * Buffers response body through `Write()` calls * Stores status code (defaults to 200 if not explicitly set) #### Envelope Construction The envelope is constructed after the handler completes: 1. **Content-Type Check**: Only wraps responses matching configured content types (default: `application/json`) 2. **Success Determination**: Status codes 200-399 are considered successful 3. **Response Parsing**: Original JSON response is unmarshaled into the data field 4. **Error Handling**: For error responses, attempts to extract error message from: * `error` field in response body * `message` field in response body * Handler error if available 5. **Metadata Addition**: When `IncludeMeta` is enabled, adds status code and request ID 6. **Re-encoding**: Final envelope is marshaled to JSON and written to the original writer #### Field Customization All field names are customizable through options: * `SuccessField`: Controls the success indicator field name * `DataField`: Controls the data payload field name * `ErrorField`: Controls the error message field name #### Content-Type Filtering The middleware only wraps responses with matching content types: * Checks exact match or with charset suffix (e.g., `application/json; charset=utf-8`) * Non-matching responses pass through unmodified * Default content types: `["application/json"]` ### Helper Functions The package provides convenience functions for common responses: * `Success()`: 200 OK with data * `Created()`: 201 Created with data * `NoContent()`: 204 No Content * `BadRequest()`: 400 Bad Request with error message * `Unauthorized()`: 401 Unauthorized with error message * `Forbidden()`: 403 Forbidden with error message * `NotFound()`: 404 Not Found with error message * `InternalError()`: 500 Internal Server Error with error message ## Best Practices * Use consistent structure across APIs * Include relevant metadata * Document envelope format * Handle nested errors properly ## Testing The envelope middleware includes comprehensive tests covering all functionality: | Test Case | Description | Expected Behavior | | ------------------------------ | ---------------------------------- | ------------------------------------------------------------ | | `TestNew` | Default middleware initialization | Wraps response with `success: true` and `data` field | | `TestWithOptions_IncludeMeta` | Metadata inclusion with request ID | Includes `meta` object with `status_code` and `request_id` | | `TestWithOptions_CustomFields` | Custom field names | Uses custom `ok`, `result`, and `message` fields | | `TestSuccess` | Success helper function | Returns 200 OK with `success: true` and data | | `TestError` | Error helper function | Returns error status with `success: false` and error message | | `TestCreated` | Created helper function | Returns 201 Created status with success response | | `TestBadRequest` | BadRequest helper function | Returns 400 Bad Request with error message | | `TestUnauthorized` | Unauthorized helper function | Returns 401 Unauthorized with error message | | `TestForbidden` | Forbidden helper function | Returns 403 Forbidden with error message | | `TestNotFound` | NotFound helper function | Returns 404 Not Found with error message | | `TestInternalError` | InternalError helper function | Returns 500 Internal Server Error with error message | | `TestNoContent` | NoContent helper function | Returns 204 No Content without body | ## Related Middlewares * [transformer](/middlewares/transformer) - Response transformation * [requestid](/middlewares/requestid) - Request IDs * [errorpage](/middlewares/errorpage) - Error pages # Error Page Source: https://docs.go-mizu.dev/middlewares/errorpage Custom error page middleware for handling HTTP errors. ## Overview The `errorpage` middleware provides custom error pages for HTTP error responses, replacing default error responses with branded pages. Use it when you need: * Custom 404 pages * Branded error pages * User-friendly error messages ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/errorpage" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(errorpage.New(errorpage.Options{ 404: errorpage.File("./errors/404.html"), 500: errorpage.File("./errors/500.html"), })) ``` ## Configuration ### Options Error code to handler mapping: ```go theme={null} type Options map[int]Handler ``` ## Examples ### HTML Files ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 404: errorpage.File("./errors/404.html"), 500: errorpage.File("./errors/500.html"), })) ``` ### Inline HTML ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 404: errorpage.HTML(`The page you requested does not exist.
`), })) ``` ### JSON Errors ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 404: errorpage.JSON(map[string]string{ "error": "Resource not found", "code": "NOT_FOUND", }), })) ``` ### Custom Handler ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 404: func(c *mizu.Ctx) error { // Log the 404 log.Printf("404: %s", c.Request().URL.Path) // Return custom response return c.HTML(404, custom404Page) }, })) ``` ### Catch All Errors ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 400: errorHandler, 401: errorHandler, 403: errorHandler, 404: errorHandler, 500: errorHandler, })) ``` ## API Reference ### Functions ```go theme={null} // New creates error page middleware func New(opts Options) mizu.Middleware // Response helpers func File(path string) Handler func HTML(content string) Handler func JSON(data any) Handler func Template(tmpl *template.Template, data any) Handler ``` ## Technical Details ### Implementation Overview The errorpage middleware uses a response writer wrapper (`statusWriter`) to intercept HTTP status codes before they are sent to the client. This allows the middleware to: 1. **Capture Status Codes**: Wraps the response writer to track status codes without immediately sending them 2. **Conditional Rendering**: Only renders error pages for status codes >= 400 when no response body has been written 3. **Template Processing**: Uses Go's `html/template` package for rendering error pages with customizable templates 4. **Default Pages**: Provides pre-configured error pages for common HTTP error codes (400, 401, 403, 404, 405, 500, 502, 503, 504) ### Response Writer Wrapper The `statusWriter` type intercepts `WriteHeader()` calls to delay status code propagation until after determining whether a custom error page should be shown: ```go theme={null} type statusWriter struct { http.ResponseWriter status int written bool } ``` This wrapper ensures that: * Status codes are captured but not immediately sent * Error pages are only shown when the response body hasn't been written * The middleware can distinguish between different error scenarios ### Error Handling Flow 1. Request enters middleware 2. Response writer is wrapped with `statusWriter` 3. Next handler is called 4. If status code >= 400 and no body written: * Custom error handler is invoked (if configured) * Custom 404 handler is invoked for 404s (if configured) * Appropriate error page is rendered using templates 5. Response is sent to client ## Best Practices * Keep error pages simple and fast * Include navigation back to home * Log errors for monitoring * Use different pages for client vs server errors ## Testing The errorpage middleware includes comprehensive test coverage for all major functionality: | Test Case | Description | Expected Behavior | | --------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------- | | `TestNew` | Tests default error page rendering | Returns 404 status and displays default "Not Found" page with error code | | `TestWithOptions_CustomPages` | Tests custom error page configuration | Renders custom title "Oops!" and message for 404 errors | | `TestWithOptions_CustomTemplate` | Tests custom HTML template | Renders error using custom template format "Error 500: ..." | | `TestWithOptions_NotFoundHandler` | Tests custom 404 handler | Returns JSON response `{"error":"not found"}` instead of HTML | | `TestWithOptions_ErrorHandler` | Tests custom error handler for all errors | Returns "Custom error handler" text for any error status | | `TestWithOptions_SuccessDoesNotShowErrorPage` | Verifies successful responses bypass error pages | Returns "success" without error page interference | | `TestNotFound` | Tests NotFound() middleware function | Returns 404 status with "Not Found" text | | `TestCustom` | Tests Custom() middleware with specific pages | Renders "Maintenance" page for 503 status | | `TestPage404` | Tests Page404 helper function | Creates page with code 404 and custom title/message | | `TestPage500` | Tests Page500 helper function | Creates page with code 500 and custom title/message | | `TestDefaultPages` | Validates all default error pages exist | Confirms default pages for codes 400, 401, 403, 404, 405, 500, 502, 503, 504 | ## Related Middlewares * [recover](/middlewares/recover) - Panic recovery * [envelope](/middlewares/envelope) - Response wrapping * [logger](/middlewares/logger) - Request logging # ETag Source: https://docs.go-mizu.dev/middlewares/etag ETag generation middleware for HTTP caching and conditional requests. ## Overview The `etag` middleware automatically generates ETag headers for responses, enabling efficient HTTP caching with conditional requests. When a client sends a matching `If-None-Match` header, it returns 304 Not Modified. Use it when you need: * Bandwidth optimization through caching * Conditional GET requests * Cache validation ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/etag" ``` ## Quick Start ```go theme={null} app := mizu.New() // Enable ETag generation app.Use(etag.New()) app.Get("/api/data", func(c *mizu.Ctx) error { return c.JSON(200, data) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------- | --------------------- | -------- | -------------------- | | `Weak` | `bool` | `false` | Generate weak ETags | | `HashFunc` | `func([]byte) string` | MD5 hash | Custom hash function | ## Examples ### Strong ETags (Default) ```go theme={null} app.Use(etag.New()) // Generates: ETag: "abc123def456" ``` ### Weak ETags ```go theme={null} app.Use(etag.Weak()) // Generates: ETag: W/"abc123def456" ``` ### Custom Hash Function ```go theme={null} app.Use(etag.WithOptions(etag.Options{ HashFunc: func(body []byte) string { h := sha256.Sum256(body) return hex.EncodeToString(h[:16]) }, })) ``` ## How It Works 1. Request comes in without `If-None-Match` 2. Response is generated with ETag header 3. Client caches response with ETag 4. Next request includes `If-None-Match: "etag-value"` 5. If content unchanged, returns 304 Not Modified 6. If changed, returns new response with new ETag ## API Reference ### Functions ```go theme={null} // New creates ETag middleware with defaults func New() mizu.Middleware // Weak creates middleware generating weak ETags func Weak() mizu.Middleware // WithOptions creates middleware with configuration func WithOptions(opts Options) mizu.Middleware ``` ## Behavior * Only generates ETags for GET and HEAD requests * Skips error responses (4xx, 5xx) * Returns 304 Not Modified for matching ETags * Supports wildcard `If-None-Match: *` * Same content always produces same ETag ## Strong vs Weak ETags | Type | Format | Use Case | | ------ | ------------ | ----------------------- | | Strong | `"abc123"` | Byte-for-byte identical | | Weak | `W/"abc123"` | Semantically equivalent | ## Technical Details The ETag middleware implementation uses a buffered writer approach to intercept and process responses: ### Response Buffering The middleware buffers the entire response body using a custom `bufferedWriter` that wraps the original `http.ResponseWriter`. This allows the middleware to: * Calculate the hash of the complete response body * Determine the final status code * Conditionally return 304 Not Modified before sending the full response ### Hash Generation By default, the middleware uses CRC32 (IEEE polynomial) for hash generation, which provides: * Fast computation suitable for HTTP caching * Sufficient uniqueness for cache validation * Low memory overhead The hash function can be customized via `HashFunc` option to use alternative algorithms like SHA256 for enhanced security or collision resistance. ### ETag Format ETags are formatted according to RFC 7232: * Strong ETags: `"We'll be back shortly.
`) }, })) ``` ### Whitelist IPs ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, Whitelist: []string{ "192.168.1.100", // Admin workstation "10.0.0.0/8", // Internal network }, })) ``` ### Whitelist Paths ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, WhitelistPaths: []string{ "/health", // Health checks "/status", // Status endpoint "/api/webhooks", // Critical webhooks }, })) ``` ### Dynamic Control ```go theme={null} mode := maintenance.NewMode(maintenance.Options{ Message: "Maintenance in progress", }) app.Use(mode.Middleware()) // Enable/disable dynamically app.Post("/admin/maintenance/enable", func(c *mizu.Ctx) error { mode.Enable() return c.Text(200, "Maintenance enabled") }) app.Post("/admin/maintenance/disable", func(c *mizu.Ctx) error { mode.Disable() return c.Text(200, "Maintenance disabled") }) app.Get("/admin/maintenance/status", func(c *mizu.Ctx) error { return c.JSON(200, map[string]bool{ "enabled": mode.IsEnabled(), }) }) ``` ### Toggle Maintenance ```go theme={null} mode := maintenance.NewMode(maintenance.Options{}) app.Post("/admin/maintenance/toggle", func(c *mizu.Ctx) error { mode.Toggle() return c.JSON(200, map[string]bool{ "enabled": mode.IsEnabled(), }) }) ``` ### Scheduled Maintenance ```go theme={null} // Maintenance window: Dec 15, 2024 2:00 AM - 4:00 AM UTC start := time.Date(2024, 12, 15, 2, 0, 0, 0, time.UTC) end := time.Date(2024, 12, 15, 4, 0, 0, 0, time.UTC) app.Use(maintenance.ScheduledMaintenance(start, end)) ``` ### Check Function ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Check: func() bool { // Check external flag (e.g., from database or config) return config.IsMaintenanceMode() }, })) ``` ### Environment-Based ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Check: func() bool { return os.Getenv("MAINTENANCE_MODE") == "true" }, WhitelistPaths: []string{"/health"}, })) ``` ### With Retry-After ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, RetryAfter: 1800, // 30 minutes })) // Response includes: Retry-After: 1800 ``` ### JSON Response ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, Handler: func(c *mizu.Ctx) error { c.Writer().Header().Set("Retry-After", "3600") return c.JSON(503, map[string]any{ "status": "maintenance", "message": "Service is under maintenance", "retry_after": 3600, }) }, })) ``` ## API Reference ```go theme={null} func New(enabled bool) mizu.Middleware func WithOptions(opts Options) mizu.Middleware func ScheduledMaintenance(start, end time.Time) mizu.Middleware func NewMode(opts Options) *Mode // Mode methods func (m *Mode) Enable() func (m *Mode) Disable() func (m *Mode) Toggle() func (m *Mode) IsEnabled() bool func (m *Mode) Middleware() mizu.Middleware ``` ## Response Headers ``` HTTP/1.1 503 Service Unavailable Retry-After: 3600 Content-Type: text/plain Service is under maintenance ``` ## Technical Details ### Implementation Architecture The maintenance middleware is implemented with a layered approach for flexibility and performance: 1. **Static Configuration** - Uses `Options` struct to configure enabled state, messages, status codes, and retry headers 2. **Dynamic Mode Control** - `Mode` type provides thread-safe atomic operations for runtime enable/disable toggling 3. **Bypass Mechanisms** - Implements two bypass layers: * IP-based whitelisting via `Whitelist` option (checks X-Forwarded-For, X-Real-IP, and RemoteAddr) * Path-based whitelisting via `WhitelistPaths` option (exact path matching) 4. **Custom Check Function** - Supports dynamic maintenance state via `Check` function, which takes precedence over static `Enabled` flag 5. **Scheduled Maintenance** - Uses time-based checks to automatically enable/disable maintenance during specific windows ### Key Components * **Options**: Configuration struct with fields for enabled state, messages, status codes, handlers, and whitelists * **Mode**: Thread-safe maintenance mode controller using `atomic.Int32` for concurrent read/write operations * **WithOptions**: Core middleware factory that processes configuration and returns the middleware function * **ScheduledMaintenance**: Helper function that creates time-windowed maintenance middleware * **getClientIP**: Helper function to extract client IP from various headers (X-Forwarded-For, X-Real-IP, RemoteAddr) * **itoa**: Custom integer-to-string conversion for Retry-After header (avoids fmt package overhead) ### Execution Flow 1. Check if maintenance is enabled (via `Check` function or `Enabled` flag) 2. If disabled, pass request to next handler 3. If enabled, check IP whitelist (bypass if matched) 4. Check path whitelist (bypass if matched) 5. Execute custom handler if provided, or return default 503 response with Retry-After header ### Thread Safety The `Mode` type uses `atomic` operations for thread-safe state management: * `Enable()` and `Disable()` use `atomic.StoreInt32` * `IsEnabled()` uses `atomic.LoadInt32` * `Toggle()` uses `atomic.CompareAndSwapInt32` for lock-free toggling ## Best Practices * Whitelist health check endpoints for monitoring * Use scheduled maintenance for planned downtime * Whitelist admin IPs for debugging * Set appropriate Retry-After values * Provide helpful maintenance messages * Use dynamic control for emergency maintenance ## Testing The maintenance middleware includes comprehensive test coverage for all scenarios: | Test Case | Description | Expected Behavior | | ------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `TestNew` | Basic maintenance mode enabled | Returns 503 with default message "Service is under maintenance" | | `TestNew_Disabled` | Maintenance mode disabled | Returns 200 and passes through to handler | | `TestWithOptions_CustomMessage` | Custom maintenance message | Returns 503 with custom message "We'll be back soon!" | | `TestWithOptions_RetryAfter` | Custom retry-after value | Sets Retry-After header to 7200 seconds | | `TestWithOptions_Whitelist` (whitelisted IP) | IP in whitelist accesses endpoint | Returns 200 and bypasses maintenance mode | | `TestWithOptions_Whitelist` (non-whitelisted IP) | IP not in whitelist accesses endpoint | Returns 503 in maintenance mode | | `TestWithOptions_WhitelistPaths` (whitelisted path) | Access to /health whitelisted path | Returns 200 and bypasses maintenance mode | | `TestWithOptions_WhitelistPaths` (non-whitelisted path) | Access to /api non-whitelisted path | Returns 503 in maintenance mode | | `TestWithOptions_CustomHandler` | Custom handler for maintenance response | Returns JSON response with custom handler | | `TestMode` | Mode creation, enable, disable, toggle | Correctly changes state: disabled by default, enabled after Enable(), disabled after Disable(), enabled after Toggle() | | `TestMode_Middleware` (disabled) | Middleware with Mode disabled | Returns 200 and passes through | | `TestMode_Middleware` (enabled) | Middleware with Mode enabled | Returns 503 in maintenance mode | | `TestScheduledMaintenance` | Scheduled maintenance in the past | Returns 200 as maintenance period has ended | ## Related Middlewares * [version](/middlewares/version) - API versioning * [ratelimit](/middlewares/ratelimit) - Rate limiting # Max Connections Source: https://docs.go-mizu.dev/middlewares/maxconns Maximum connections limit middleware for resource protection. ## Overview The `maxconns` middleware limits the maximum number of concurrent connections to protect server resources. Use it when you need: * Connection limiting * Resource protection * DoS mitigation ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/maxconns" ``` ## Quick Start ```go theme={null} app := mizu.New() // Limit to 1000 concurrent connections app.Use(maxconns.New(1000)) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ----------------------- | -------- | -------------------- | | `Max` | `int` | Required | Max connections | | `ErrorHandler` | `func(*mizu.Ctx) error` | 503 | Custom error handler | ## Examples ### Basic Limit ```go theme={null} app.Use(maxconns.New(1000)) ``` ### Custom Error ```go theme={null} app.Use(maxconns.New(maxconns.Options{ Max: 1000, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(503, map[string]string{ "error": "Server at capacity", }) }, })) ``` ### With Monitoring ```go theme={null} m := maxconns.NewWithMetrics(1000) app.Use(m.Middleware()) // Monitor in handler app.Get("/stats", func(c *mizu.Ctx) error { return c.JSON(200, map[string]int{ "active": m.Active(), "max": m.Max(), }) }) ``` ## API Reference ### Functions ```go theme={null} // New creates max connections middleware func New(max int) mizu.Middleware // NewWithOptions creates with configuration func NewWithOptions(opts Options) mizu.Middleware // NewWithMetrics creates with metrics tracking func NewWithMetrics(max int) *MaxConns ``` ## Technical Details ### Implementation Architecture The maxconns middleware uses atomic operations and mutexes to safely manage concurrent connections: **Global Connection Tracking:** * Uses `atomic.LoadInt64` and `atomic.AddInt64` for lock-free global connection counting * Atomic operations ensure thread-safe increments/decrements without mutex overhead * Counter is incremented before processing and decremented via defer after completion **Per-IP Connection Limiting:** * Maintains a `map[string]int` for per-IP connection counts * Protected by `sync.RWMutex` to allow concurrent reads with exclusive writes * IP addresses extracted from `X-Forwarded-For`, `X-Real-IP`, or `RemoteAddr` headers * Map entries automatically cleaned up when IP count reaches zero **Connection Lifecycle:** 1. Check global limit using atomic load operation 2. If PerIP enabled, check IP-specific limit under read lock 3. Increment both counters (atomic for global, locked for per-IP) 4. Process request through handler chain 5. Decrement counters via defer (guarantees cleanup even on errors) **Zero/Negative Max Handling:** * When `Max <= 0`, middleware immediately rejects all requests * Optimized fast path that bypasses counter management **Error Handling:** * Default response: 503 Service Unavailable with "Retry-After: 60" header * Custom error handlers can be configured via Options * Error handler receives full context for custom responses ### Key Components **Counter Type:** * Provides observable connection metrics * Exposes `Current()` and `Max()` methods for monitoring * Can be used standalone for custom monitoring solutions **Helper Functions:** * `Global(max)`: Alias for `New(max)` for semantic clarity * `PerIP(max)`: Convenience function setting per-IP limit with high global max (1,000,000) * `getClientIP()`: Extracts client IP from standard proxy headers ## Best Practices * Set based on server capacity * Monitor connection counts * Use with rate limiting * Return appropriate error response ## Testing The maxconns middleware includes comprehensive test coverage for all major scenarios: | Test Case | Description | Expected Behavior | | ------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------- | | `TestNew` | Concurrent requests exceeding max limit | Accepts up to max connections, rejects excess with 503 | | `TestWithOptions_PerIP` | Multiple concurrent requests from same IP with PerIP limit | Only allows configured number per IP, rejects others | | `TestWithOptions_CustomErrorHandler` | Custom error handler with Max=0 | Uses custom handler returning custom status/response | | `TestPerIP` | Sequential requests with PerIP limit | Sequential requests succeed as connections are released | | `TestGlobal` | Global limit using Global() helper | Request succeeds within global limit | | `TestCounter` | Counter type for monitoring | Counter tracks current connections and exposes metrics | | `TestRetryAfterHeader` | Rejected request header validation | Sets "Retry-After: 60" header on 503 responses | ## Related Middlewares * [bulkhead](/middlewares/bulkhead) - Bulkhead pattern * [ratelimit](/middlewares/ratelimit) - Rate limiting * [keepalive](/middlewares/keepalive) - Connection persistence # Method Override Source: https://docs.go-mizu.dev/middlewares/methodoverride HTTP method override middleware for HTML forms. ## Overview The `methodoverride` middleware allows HTML forms to use HTTP methods other than GET and POST. Since HTML forms only support GET and POST, this middleware lets you override the method using a header or form field. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/methodoverride" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(methodoverride.New()) ``` ## Configuration | Option | Type | Default | Description | | ----------- | ---------- | ---------------------------- | ------------------------ | | `Header` | `string` | `"X-HTTP-Method-Override"` | Override header | | `FormField` | `string` | `"_method"` | Form field name | | `Methods` | `[]string` | `["PUT", "PATCH", "DELETE"]` | Allowed override methods | ## Examples ### HTML Form with PUT ```html theme={null} ``` ```go theme={null} app.Use(methodoverride.New()) app.Put("/users/:id", updateUser) // Handles the form above ``` ### DELETE via Form ```html theme={null} ``` ### Header Override (AJAX) ```javascript theme={null} fetch('/users/123', { method: 'POST', headers: { 'X-HTTP-Method-Override': 'PUT', 'Content-Type': 'application/json' }, body: JSON.stringify({name: 'John'}) }); ``` ### Custom Configuration ```go theme={null} app.Use(methodoverride.WithOptions(methodoverride.Options{ Header: "X-Method", FormField: "method", Methods: []string{"PUT", "DELETE"}, // Only allow PUT/DELETE })) ``` ## How It Works 1. Only processes POST requests 2. Checks header, then query, then form field for override value 3. If valid method found, changes request method 4. Subsequent handlers see the overridden method ## API Reference ```go theme={null} func New() mizu.Middleware func WithOptions(opts Options) mizu.Middleware ``` ## Technical Details ### Implementation Overview The method override middleware works by intercepting POST requests and checking for method override indicators in the following priority order: 1. **Header Check**: First checks the override header (default: `X-HTTP-Method-Override`) 2. **Query Parameter**: If no header is found, checks the query parameter with the form field name 3. **Form Field**: For form submissions (content types `application/x-www-form-urlencoded` or `multipart/form-data`), checks the form field value ### Method Validation * Override values are converted to uppercase for case-insensitive matching * Only methods specified in the `Methods` configuration are allowed (default: PUT, PATCH, DELETE) * Invalid or disallowed methods are silently ignored, leaving the original POST method intact * The allowed methods are stored in a map for O(1) lookup performance ### Request Processing The middleware only processes POST requests. Any other HTTP method (GET, PUT, DELETE, etc.) passes through unchanged, even if override headers or parameters are present. This prevents potential security issues from unexpected method changes. ### Content Type Handling Form field checking only occurs when the request has an appropriate content type: * `application/x-www-form-urlencoded` * `multipart/form-data` This ensures form parsing is only attempted for actual form submissions, avoiding unnecessary processing for JSON or other content types. ## Best Practices ### Use CSRF Protection Always combine method override with CSRF protection when handling form submissions: ```go theme={null} app.Use(csrf.New()) app.Use(methodoverride.New()) ``` ### Restrict Allowed Methods Only enable the HTTP methods your application actually needs: ```go theme={null} app.Use(methodoverride.WithOptions(methodoverride.Options{ Methods: []string{"PUT", "DELETE"}, // Exclude PATCH if not needed })) ``` ### Header Priority When using AJAX requests, prefer the header approach for cleaner code and better separation from form data. ## Testing The middleware includes comprehensive test coverage for all override scenarios: | Test Case | Description | Expected Behavior | | ------------------------ | ------------------------------------------------------------------------ | --------------------------------------------- | | Header Override | POST request with `X-HTTP-Method-Override: PUT` header | Method changed to PUT | | Query Override | POST request with `?_method=DELETE` query parameter | Method changed to DELETE | | Form Override | POST request with `_method=PATCH` form field | Method changed to PATCH | | Non-POST Ignored | GET request with override header | Method remains GET, override ignored | | Invalid Method | POST request with `X-HTTP-Method-Override: INVALID` | Method remains POST, invalid override ignored | | Custom Header | POST request with custom header name (`X-Method: PUT`) | Method changed to PUT using custom header | | Custom Methods - Allowed | POST request with allowed custom method (GET) | Method changed to GET | | Custom Methods - Blocked | POST request with non-allowed method (DELETE) when only GET/HEAD allowed | Method remains POST | | Case Insensitive | POST request with lowercase override value (`delete`) | Method changed to DELETE (uppercase) | ## Security Notes * Only POST requests can be overridden * Only allowed methods (PUT, PATCH, DELETE by default) can be used * Use with CSRF protection for forms ## Related Middlewares * [csrf](/middlewares/csrf) - Form protection # Metrics Source: https://docs.go-mizu.dev/middlewares/metrics Custom metrics collection middleware for application monitoring. ## Overview The `metrics` middleware collects custom application metrics including request counts, latencies, and error rates. Use it when you need: * Application monitoring * Custom metric collection * Performance tracking ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/metrics" ``` ## Quick Start ```go theme={null} app := mizu.New() m := metrics.New() app.Use(m.Middleware()) // Expose metrics endpoint app.Get("/metrics", m.Handler()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ----------- | ----------- | -------- | ---------------- | | `Namespace` | `string` | `"http"` | Metric namespace | | `Buckets` | `[]float64` | Standard | Latency buckets | ## Examples ### Basic Metrics ```go theme={null} m := metrics.New() app.Use(m.Middleware()) app.Get("/metrics", m.Handler()) ``` ### Custom Namespace ```go theme={null} m := metrics.New(metrics.Options{ Namespace: "myapp", }) ``` ### Custom Buckets ```go theme={null} m := metrics.New(metrics.Options{ Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1, 5}, }) ``` ### Add Custom Metrics ```go theme={null} m := metrics.New() // Counter m.Counter("jobs_processed", "Total jobs processed") m.Inc("jobs_processed") // Gauge m.Gauge("active_connections", "Active connections") m.Set("active_connections", 42) // Histogram m.Histogram("job_duration", "Job duration", buckets) m.Observe("job_duration", 1.5) ``` ## Default Metrics | Metric | Type | Description | | ------------------------------- | --------- | --------------- | | `http_requests_total` | Counter | Total requests | | `http_request_duration_seconds` | Histogram | Request latency | | `http_requests_in_flight` | Gauge | Active requests | ## API Reference ### Functions ```go theme={null} // New creates metrics collector func New(opts ...Options) *Metrics // Middleware returns metrics middleware func (m *Metrics) Middleware() mizu.Middleware // Handler returns metrics HTTP handler func (m *Metrics) Handler() mizu.Handler // Custom metrics func (m *Metrics) Counter(name, help string) func (m *Metrics) Gauge(name, help string) func (m *Metrics) Histogram(name, help string, buckets []float64) func (m *Metrics) Inc(name string) func (m *Metrics) Set(name string, value float64) func (m *Metrics) Observe(name string, value float64) ``` ## Technical Details ### Core Components The metrics middleware consists of the following key components: #### Metrics Structure * **RequestCount**: Total number of HTTP requests processed (atomic int64) * **ErrorCount**: Total number of errors (4xx and 5xx status codes, atomic int64) * **TotalDuration**: Cumulative duration of all requests in nanoseconds (atomic int64) * **ActiveRequests**: Number of currently in-flight requests (atomic int64) * **statusCodes**: Map tracking counts for each HTTP status code * **pathCounts**: Map tracking request counts per URL path #### Thread Safety All counters use atomic operations (`atomic.AddInt64`, `atomic.LoadInt64`, `atomic.StoreInt64`) to ensure thread-safe concurrent access without explicit locking for the main counters. Maps (`statusCodes` and `pathCounts`) are protected by a `sync.RWMutex` to safely handle concurrent reads and writes. #### Status Capture Mechanism The middleware uses a custom `statusCapture` wrapper that implements `http.ResponseWriter` to intercept the status code before writing the response. This allows tracking of status codes even when they're set by downstream handlers. #### Metrics Calculation * **Average Duration**: Calculated as `TotalDuration / RequestCount` and converted to milliseconds * **Error Detection**: Requests are counted as errors when `err != nil` or `statusCode >= 400` ### Output Formats The middleware supports two output formats: 1. **JSON Format** (`Handler()`): Returns statistics as JSON with fields: * `request_count`, `error_count`, `active_requests` * `average_duration_ms`, `status_codes`, `path_counts` 2. **Prometheus Format** (`Prometheus()`): Exports metrics in Prometheus text format: * `http_requests_total` (counter) * `http_errors_total` (counter) * `http_active_requests` (gauge) ### Implementation Notes * Uses custom `itoa()` function for integer to string conversion to avoid allocations * Metrics can be reset using `Reset()` method which clears all counters and maps * The middleware does not affect the response or request flow, only observes them ## Best Practices * Use meaningful metric names * Add labels for dimensions * Set appropriate histogram buckets * Monitor metric cardinality ## Testing The metrics middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ----------------------------- | ----------------------------------------- | ---------------------------------------------------------------- | | `TestNew` | Basic initialization and request counting | Creates middleware, counts single request correctly | | `TestMetrics_StatusCodes` | HTTP status code tracking | Tracks 200 and 500 status codes separately, counts 500 as error | | `TestMetrics_PathCounts` | URL path request counting | Counts requests per path (3 to /api/users, 1 to /api/posts) | | `TestMetrics_Handler` | JSON metrics endpoint | Returns metrics as JSON with correct Content-Type header | | `TestMetrics_Prometheus` | Prometheus format export | Returns Prometheus-formatted metrics with http\_requests\_total | | `TestMetrics_Reset` | Metrics reset functionality | Resets all counters to zero after processing requests | | `TestMetrics_Concurrent` | Thread-safe concurrent access | Correctly counts 100 concurrent requests without race conditions | | `TestMetrics_AverageDuration` | Duration measurement | Calculates positive average duration for requests | | `TestMetrics_JSON` | JSON serialization | Marshals stats to JSON with request\_count field | ## Related Middlewares * [prometheus](/middlewares/prometheus) - Prometheus format * [timing](/middlewares/timing) - Server-Timing header * [healthcheck](/middlewares/healthcheck) - Health checks # Mirror Source: https://docs.go-mizu.dev/middlewares/mirror Request mirroring middleware for traffic shadowing. ## Overview The `mirror` middleware duplicates requests to one or more target servers for traffic shadowing, testing, and analysis. Mirrored requests run asynchronously and don't affect the original response. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/mirror" ``` ## Quick Start ```go theme={null} app := mizu.New() // Mirror all traffic to staging server app.Use(mirror.New("https://staging.example.com")) ``` ## Configuration | Option | Type | Default | Description | | ----------- | ------------------------------ | ------- | ---------------------------------- | | `Targets` | `[]Target` | - | Mirror target URLs | | `Timeout` | `time.Duration` | `5s` | Timeout for mirrored requests | | `Async` | `bool` | `true` | Run mirror requests asynchronously | | `CopyBody` | `bool` | `true` | Copy request body | | `OnError` | `func(string, error)` | - | Error callback | | `OnSuccess` | `func(string, *http.Response)` | - | Success callback | ## Examples ### Single Target ```go theme={null} app.Use(mirror.New("https://staging.example.com")) // All requests are mirrored to staging app.Get("/api/users", usersHandler) ``` ### Multiple Targets ```go theme={null} app.Use(mirror.New( "https://staging.example.com", "https://analytics.example.com", )) ``` ### Percentage-Based Mirroring ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ mirror.Percentage("https://staging.example.com", 50), // 50% of traffic mirror.Percentage("https://canary.example.com", 10), // 10% of traffic }, })) ``` ### Custom Timeout ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, Timeout: 10 * time.Second, })) ``` ### Synchronous Mirroring ```go theme={null} // Wait for mirror requests to complete app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://backup.example.com", Percentage: 100}, }, Async: false, })) ``` ### Error Handling ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, OnError: func(target string, err error) { log.Printf("Mirror to %s failed: %v", target, err) metrics.IncrementCounter("mirror_errors", target) }, })) ``` ### Success Callback ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, OnSuccess: func(target string, resp *http.Response) { log.Printf("Mirror to %s: status %d", target, resp.StatusCode) // Compare responses if resp.StatusCode >= 500 { alertOps("Staging returned error") } }, })) ``` ### Response Comparison ```go theme={null} var mu sync.Mutex var prodResponses = make(map[string]int) var stagingResponses = make(map[string]int) app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, OnSuccess: func(target string, resp *http.Response) { mu.Lock() defer mu.Unlock() stagingResponses[resp.Request.URL.Path] = resp.StatusCode }, })) // Middleware to track production responses app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { err := next(c) // Track and compare... return err } }) ``` ### Route-Specific Mirroring ```go theme={null} // Only mirror specific routes api := app.Group("/api") api.Use(mirror.New("https://staging.example.com")) // Other routes are not mirrored app.Get("/health", healthHandler) ``` ### Canary Testing ```go theme={null} // Mirror small percentage to canary for testing app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ mirror.Percentage("https://canary.example.com", 5), }, OnSuccess: func(target string, resp *http.Response) { if resp.StatusCode >= 500 { // Alert on canary errors alertCanaryError(target, resp.StatusCode) } }, OnError: func(target string, err error) { alertCanaryError(target, -1) }, })) ``` ### A/B Testing Analysis ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://experiment-a.example.com", Percentage: 50}, {URL: "https://experiment-b.example.com", Percentage: 50}, }, OnSuccess: func(target string, resp *http.Response) { // Log response times for analysis latency := resp.Header.Get("X-Response-Time") analytics.TrackLatency(target, latency) }, })) ``` ### Skip Body for GET Requests ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, CopyBody: false, // Don't copy body (faster for GET) })) ``` ## Target Structure ```go theme={null} type Target struct { URL string // Target base URL Percentage int // Percentage of requests to mirror (0-100) } ``` ## Helper Functions ```go theme={null} // Create target with percentage mirror.Percentage("https://example.com", 50) // 50% of traffic ``` ## Mirrored Request Headers Each mirrored request includes: * All original headers * `X-Mirrored-From: