Skip to content

veb #

veb - the V Web Server

A simple yet powerful web server with built-in routing, parameter handling, templating, and other features.

Generate client-ready API specifications with typed OpenAPI documents.

Features

  • Very fast performance of C on the web.
  • Templates are precompiled all errors are visible at compilation time, not at runtime.
  • Middleware functionality similar to other big frameworks.
  • HTTPS support directly from veb via mbedtls.
  • Method-aware route middleware for protecting only selected HTTP verbs.
  • Static compression with gzip/zstd, including manual pre-compression support.
  • Controllers to split up your apps logic.
  • Graceful shutdown support in the new_veb backend.
  • Easy to deploy just one binary file that also includes all templates. No need to install any dependencies.

Quick Start

Run your veb app with a live reload via v -d veb_livereload watch run . Compiler options before watch are forwarded to the app's compiler command. Options after watch, such as v watch -d veb_livereload run ., also work.

Stylesheet changes are applied in place. Changes to other static assets trigger a full reload so updated scripts and images are fetched. Page or template changes preserve the DOM state unless scripts or head elements changed; those changes also reload the page so the browser applies them.

Page and template edits update the browser while preserving scroll position, focus, and typed form values when scripts and head elements stay the same. No need to quit the app, rebuild it, and refresh the page in the browser!

  • A .v file or a template (anywhere under templates/): v watch rebuilds and restarts the app, then the page fetches itself again and updates only what changed. The previous version keeps serving while the new one builds. If the build fails, it keeps running, and the page shows the compiler errors until the next successful build. v watch --no-build-first stops the app before each build instead.
  • A file served by a veb.StaticHandler (for example static/css/main.css): the running app notices the change itself, with no rebuild. Stylesheets are swapped in place; other files reload the page.

Note: this works by injecting a small <script> tag before the ending </html> tag of the page, that keeps one Server-Sent Events connection open to the app. In other words, it works for template pages that have that </html> tag, or for ones that were produced by ctx.html() and that also have that closing tag, but not for ones produced by ctx.text() . Pages that were the result of a POST request, and pages that fail to update, are reloaded instead. A small notice in the corner of the page shows when the app is rebuilding and when the page was updated; hide it with -d veb_livereload_no_toast.

The live updates are not available for apps served over HTTPS (veb.run_at with an ssl_config), or behind a proxy that blocks long-lived connections. Those can keep using the older script, which polls the app and reloads the whole page after a restart: v watch -d veb_livereload -d veb_livereload_polling run . (set its interval with -d veb_livereload_poll_ms=500).

While the app restarts, the page retries the connection every 250 ms; each failed attempt shows up in the browser's network log. With v watch --no-build-first, the app is down for the whole build: -d veb_livereload_retry_max_ms=1000 makes the retries back off, doubling from 250 ms up to that cap, for fewer failed attempts at the cost of a slower update.

Port already in use

When lsof finds a process listening on the requested TCP port, veb displays its PID and asks whether to terminate it. Reply y to send the process SIGTERM; any other response leaves it running.

Deploying veb apps

All the code, including HTML templates, is in one binary file. That's all you need to deploy. Use the -prod flag when building for production.

Getting Started

To start, you must import the module veb and define a structure which will represent your app and a structure which will represent the context of a request. These structures must be declared with the pub keyword.

Example:

module main

import veb

pub struct User {
pub mut:
    name string
    id   int
}

// Our context struct must embed `veb.Context`!
pub struct Context {
    veb.Context
pub mut:
    // In the context struct we store data that could be different
    // for each request. Like a User struct or a session id
    user       User
    session_id string
}

pub struct App {
pub:
    // In the app struct we store data that should be accessible by all endpoints.
    // For example, a database or configuration values.
    secret_key string
}

// This is how endpoints are defined in veb. This is the index route
pub fn (app &App) index(mut ctx Context) veb.Result {
    return ctx.html('<html><body><h1>Hello V!</h1><p>The secret key is "${app.secret_key}"</body></html>')
}

fn main() {
    mut app := &App{
        secret_key: 'secret'
    }
    // Pass the App and context type and start the web server on port 8080
    veb.run[App, Context](mut app, 8080)
}

You can use the App struct for data you want to keep during the lifetime of your program, or for data that you want to share between different routes.

A new Context struct is created every time a request is received, so it can contain different data for each request.

Parallel picoev workers

The default non-SSL picoev backend can start more than one event loop by setting nr_workers in RunParams:

module main

import runtime
import veb

pub struct Context {
    veb.Context
}

pub struct App {}

pub fn (app &App) index(mut ctx Context) veb.Result {
    return ctx.text('Hello from parallel veb')
}

fn main() {
    mut app := &App{}
    veb.run_at[App, Context](mut app,
        host:       '0.0.0.0'
        port:       8080
        nr_workers: runtime.nr_jobs()
    ) or { panic(err) }
}

nr_workers defaults to 1 to preserve the historical single-loop behavior. It controls the default non-SSL picoev backend (currently on Linux or Termux). With -d use_openssl, it also limits concurrent TLS handshakes, so the default serializes handshakes. When running with -d new_veb, the fasthttp backend is already multi-threaded and ignores nr_workers.

Request-scoped allocation with -prealloc

When a veb app runs on the fasthttp backend and is compiled with -prealloc, each request is handled inside a scoped prealloc arena created by fasthttp. V allocations made while decoding the request, creating the veb.Context, matching routes, running middleware, rendering templates, and serializing the response all use that request arena.

The arena is freed after the response has been sent, not merely when the route handler returns. On macOS and BSD, the response buffer can be retained by the connection while kqueue finishes writing it; the arena is detached from the request thread and freed after the write completes. This keeps request-local allocations cheap while preserving response-buffer lifetime.

When a handler starts V spawn work while the request scope is active, the generated thread wrapper retains the request arena until the spawned function returns, so veb app code does not need to manually copy request strings just to pass them to a background task. Void spawned functions also run inside their own scoped arena, which is freed at thread exit. Do not store request-scoped strings, arrays, maps, Context values, or template output in app fields, globals, or caches unless you deliberately copy them into longer-lived storage. Process startup data, route tables, static file maps, database pools, and allocations made directly by C libraries are not part of the per-request arena.

To trace request arena allocation and free points while developing, build with:

v -prealloc -d trace_prealloc -d new_veb run .

HTTPS

To serve HTTPS directly from veb, pass an SSL configuration in RunParams. The default backend expects mbedtls.SSLConnectConfig. When compiling with -d use_openssl, use openssl.SSLConnectConfig instead. For example, run the following OpenSSL-backed server with v -d use_openssl run .:

module main

import net.openssl
import veb

pub struct Context {
    veb.Context
}

pub struct App {}

pub fn (app &App) index(mut ctx Context) veb.Result {
    return ctx.text('Hello over HTTPS')
}

fn main() {
    mut app := &App{}
    veb.run_at[App, Context](mut app,
        host:       '0.0.0.0'
        port:       8443
        family:     .ip
        ssl_config: openssl.SSLConnectConfig{
            cert:     'certs/server.crt'
            cert_key: 'certs/server.key'
        }
    ) or { panic(err) }
}

Graceful shutdown

When running with -d new_veb, the underlying server supports graceful shutdown. Once shutdown begins, it stops accepting new requests, waits for in-flight requests to finish, and only then exits. This is useful for deploys, tests, and management endpoints that trigger a stop after sending a response.

You can store the server handle in your app by adding an optional init_server(server &veb.Server) method:

module main

import veb

pub struct App {
pub mut:
    server &veb.Server = unsafe { nil }
}

pub fn (mut app App) init_server(server &veb.Server) {
    app.server = server
}

veb.Server forwards wait_till_running() and shutdown(timeout: ...) to the new_veb backend. These lifecycle controls are available only when running with -d new_veb without SSL.

Defining endpoints

To add endpoints to your web server, you must extend the App struct. For routing you can either use auto-mapping of function names or specify the path as an attribute. The function expects a parameter of your Context type and a response of the type veb.Result (or !veb.Result when the endpoint can fail, see Handlers that return !veb.Result).

Example:

// This endpoint can be accessed via http://server:port/hello
pub fn (app &App) hello(mut ctx Context) veb.Result {
    return ctx.text('Hello')
}

// This endpoint can be accessed via http://server:port/foo
@['/foo']
pub fn (app &App) world(mut ctx Context) veb.Result {
    return ctx.text('World')
}

HTTP verbs

To use any HTTP verbs (or methods, as they are properly called), such as @[post], @[get], @[put], @[patch] or @[delete] you can simply add the attribute before the function definition.

Example:

// only GET requests to http://server:port/world are handled by this method
@[get]
pub fn (app &App) world(mut ctx Context) veb.Result {
    return ctx.text('World')
}

// only POST requests to http://server:port/product/create are handled by this method
@['/product/create'; post]
pub fn (app &App) create_product(mut ctx Context) veb.Result {
    return ctx.text('product')
}

By default, endpoints are marked as GET requests only. It is also possible to add multiple HTTP verbs per endpoint.

Example:

// only GET and POST requests to http://server:port/login are handled by this method
@['/login'; get; post]
pub fn (app &App) login(mut ctx Context) veb.Result {
    if ctx.req.method == .get {
        // show the login page on a GET request
        return ctx.html('<h1>Login page</h1><p>todo: make form</p>')
    } else {
        // request method is POST
        password := ctx.form['password']
        // validate password length
        if password.len < 12 {
            return ctx.text('password is too weak!')
        } else {
            // we receive a POST request, so we want to explicitly tell the browser
            // to send a GET request to the profile page.
            return ctx.redirect('/profile')
        }
    }
}

Request forms

For POST, PUT and PATCH requests, ctx.form contains decoded form fields. With Content-Type: application/json, a JSON object supplies its string, number and boolean fields as strings: {"name":"x","id":3} becomes {'name': 'x', 'id': '3'}. Media type matching ignores case and accepts parameters such as charset=utf-8. Whitespace around a JSON object is accepted. Nested objects, arrays and null fields are skipped; use ctx.req.data or ctx.bind[T]() when you need the full structure. The original request body remains available in ctx.req.data. Malformed JSON and a root value other than an object, including null, receive 400 Bad Request.

URL-encoded bodies continue to populate ctx.form, and multipart requests also populate ctx.files. A JSON body requires its JSON content type; veb does not infer it from the body.

Routes with Parameters

Parameters are passed directly to an endpoint route using the colon sign :. The route parameters are passed as arguments. V will cast the parameter to any of V's primitive types (string, int etc,).

To pass a parameter to an endpoint, you simply define it inside an attribute, e. g. @['/hello/:user]. After it is defined in the attribute, you have to add it as a function parameter. Parameters after ctx are populated from strings, so they currently must be string, integer, or bool.

Example:

// V will pass the parameter 'user' as a string
           vvvv
@['/hello/:user']                             vvvv
pub fn (app &App) hello_user(mut ctx Context, user string) veb.Result {
    return ctx.text('Hello ${user}')
}

// V will pass the parameter 'id' as an int
              vv
@['/document/:id']                              vv
pub fn (app &App) get_document(mut ctx Context, id int) veb.Result {
    return ctx.text('Document ${id}')
}

If we visit http://localhost:port/hello/vaesel we would see the text Hello vaesel.

Routes with Parameter Arrays

If you want multiple parameters in your route and if you want to parse the parameters yourself, or you want a wildcard route, you can add ... after the : and name, e.g. @['/:path...'].

This will match all routes after '/'. For example, the url /path/to/test would give path = '/path/to/test'.

         vvv
@['/:path...']                              vvvv
pub fn (app &App) wildcard(mut ctx Context, path string) veb.Result {
    return ctx.text('URL path = "${path}"')
}

Query, Form and Files

You have direct access to query values by accessing the query field on your context struct. You are also able to access any formdata or files that were sent with the request with the fields .form and .files respectively.

For routes without URL parameters, arguments after ctx are populated by name from GET query values or POST form values. This also works for the index route at /. Missing values become empty strings, or zero values after conversion to integer or bool.

@['/forms'; post]
pub fn (app &App) forms(mut ctx Context, value string) veb.Result {
    return ctx.text(value)
}

Posting value=hello as application/x-www-form-urlencoded to /forms passes hello to the handler.

In the following example, visiting http://localhost:port/user?name=veb we will see the text Hello veb!. And if we access the route without the name parameter, http://localhost:port/user, we will see the text no user was found,

Example:

@['/user'; get]
pub fn (app &App) get_user_by_id(mut ctx Context) veb.Result {
    user_name := ctx.query['name'] or {
        // we can exit early and send a different response if no `name` parameter was passed
        return ctx.text('no user was found')
    }

    return ctx.text('Hello ${user_name}!')
}

Request body size

veb reads the whole request body into memory before it calls the route. A body can have up to max_request_body_size bytes, a field of RunParams that is 64 MiB by default. veb answers a request that has a larger body with 413, and closes the connection. Raise the limit to accept larger uploads:

veb.run_at[App, Context](mut app,
    port:                  8080
    max_request_body_size: 512 * 1024 * 1024
)!

max_request_body_size: 0 removes the limit; use it only where every client is trusted. A negative value is an error. max_request_buffer_size is a different setting, which does not limit the body.

The limit applies to both plain HTTP and HTTPS. Chunked bodies count the combined payload of all chunks. Invalid or overflowing Content-Length values and malformed chunk sizes are rejected with 400 before allocating their declared body; TLS reads also reject truncated bodies. Conflicting lengths and requests containing both Transfer-Encoding and Content-Length are rejected with 400.

To give some routes a lower limit, register veb.body_limit on them with route_use. It answers a larger body with 413 without closing the connection. It runs after veb has read the body, so it does not lower the memory that a request can use:

app.route_use('/api/profile', veb.body_limit[Context](100 * 1024))

Host

To restrict an endpoint to a specific host, you can use the host attribute followed by a colon : and the host name. You can test the Host feature locally by adding a host to the "hosts" file of your device.

Example:

@['/'; host: 'example.com']
pub fn (app &App) hello_web(mut ctx Context) veb.Result {
    return ctx.text('Hello WEB')
}

@['/'; host: 'api.example.org']
pub fn (app &App) hello_api(mut ctx Context) veb.Result {
    return ctx.text('Hello API')
}

// define the handler without a host attribute last if you have conflicting paths.
@['/']
pub fn (app &App) hello_others(mut ctx Context) veb.Result {
    return ctx.text('Hello Others')
}

You can also create a controller to handle all requests from a specific host in one app struct.

Route Matching Order

veb will match routes in the order that you define endpoints.

Example:

@['/:path']
pub fn (app &App) with_parameter(mut ctx Context, path string) veb.Result {
    return ctx.text('from with_parameter, path: "${path}"')
}

@['/normal']
pub fn (app &App) normal(mut ctx Context) veb.Result {
    return ctx.text('from normal')
}

In this example we defined an endpoint with a parameter first. If we access our app on the url http://localhost:port/normal we will not see from normal, but from with_parameter, path: "normal".

Routes with a variadic parameter such as /:path... are an exception. veb keeps them as fallbacks, so a later exact or non-variadic parameter route can still match before the variadic route handles the request.

If the path matches a route for the request's host but the method does not, veb answers 405 Method Not Allowed. Its Allow header lists the distinct methods from all handlers for the selected route pattern, including handlers whose path parameters use different names. Methods from other hosts or overlapping route patterns are excluded. A path with no matching route still answers 404 Not Found.

Custom not found page

You can implement a not_found endpoint that is called when a request is made, and no matching route is found to replace the default HTTP 404 not found page. This route has to be defined on our Context struct.

Example:

pub fn (mut ctx Context) not_found() veb.Result {
    // set HTTP status 404
    ctx.res.set_status(.not_found)
    return ctx.html('<h1>Page not found!</h1>')
}

Rendering without a server

veb.render_request[A, X](mut app, req) dispatches an http.Request through the same routes, middleware, and registered controllers as the server, without opening a listener. It returns !&veb.Context: read the completed response from ctx.res. X must embed veb.Context. Context, route, and controller configuration errors are returned to the caller. Custom fields on X remain local to the handler; they are not part of the returned context.

ctx := veb.render_request[App, Context](mut app, http.Request{
    method: .get
    url: '/'
})!
os.write_file('index.html', ctx.res.body)!

Initialize static handlers and controllers before rendering, as before veb.run. Static file responses expose ctx.return_file and ctx.return_type instead of reading the file into ctx.res.body. Handlers that take over a connection require a real server connection and are unsuitable for this helper.

See examples/veb/static_export/ for an HTML export using the same page handlers as its preview server.

Static files and website

veb also provides a way of handling static files. We can mount a folder at the root of our web app, or at a custom route. To start using static files we have to embed veb.StaticHandler on our app struct.

Example:

Let's say you have the following file structure:

.
├── static/
│   ├── css/
│   │   └── main.css
│   └── js/
│       └── main.js
└── main.v

If we want all the documents inside the static sub-directory to be publicly accessible, we can use handle_static.

Note:
veb will recursively search the folder you mount; all the files inside that folder
will be publicly available.

main.v

module main

import veb

pub struct Context {
    veb.Context
}

pub struct App {
    veb.StaticHandler
}

fn main() {
    mut app := &App{}

    app.handle_static('static', false)!

    veb.run[App, Context](mut app, 8080)
}

If we start the app with v run main.v we can access our main.css file at http://localhost:8080/static/css/main.css

Mounting folders at specific locations

In the previous example the folder static was mounted at /static. We could also choose to mount the static folder at the root of our app: everything inside the static folder is available at /.

Example:

// change the second argument to `true` to mount a folder at the app root
app.handle_static('static', true)!

We can now access main.css directly at http://localhost:8080/css/main.css.

If a request is made to the root of a static folder, veb will look for an index.html or ìndex.htm file and serve it if available. Thus, it's also a good way to host a complete website. An example is available here.

It is also possible to mount the static folder at a custom path.

Example:

// mount the folder 'static' at path '/public', the path has to start with '/'
app.mount_static_folder_at('static', '/public')

If we run our app the main.css file is available at http://localhost:8080/public/main.css

Adding a single static asset

If you don't want to mount an entire folder, but only a single file, you can use serve_static.

Example:

// serve the `main.css` file at '/path/main.css'
app.serve_static('/path/main.css',  'static/css/main.css')!

Dealing with MIME types

By default, veb will map the extension of a file to a MIME type. If any of your static file's extensions do not have a default MIME type in veb, veb will throw an error and you have to add your MIME type to .static_mime_types yourself.

Example:

Given the following file structure:

.
├── static/
│   └── file.what
└── main.v
app.handle_static('static', true)!

This code will throw an error, because veb has no default MIME type for a .what file extension.

unknown MIME type for file extension ".what"

To fix this we have to provide a MIME type for the .what file extension:

app.static_mime_types['.what'] = 'txt/plain'
app.handle_static('static', true)!

Compression for static files (zstd/gzip)

veb provides automatic compression (zstd and gzip) for static files with smart caching. When enabled, veb will serve compressed versions of your static files to clients that support compression, reducing bandwidth usage and improving load times. Zstd is preferred over gzip when the client supports both.

How it works:

  1. Manual pre-compression: If you create .zst or .gz files manually, veb will serve them in zero-copy streaming mode for maximum performance when the MIME type is allowed.
  2. Lazy compression cache: Files smaller than the threshold are automatically compressed on first request and cached under os.cache_dir()/veb/static_compression/ (zstd preferred when the client supports it). The source directory stays unchanged.
  3. Cache validation: If the original file is modified, the compressed cache is automatically regenerated on the next request.
  4. Streaming for large files: Files larger than the threshold are served uncompressed in streaming mode (unless a manual .zst or .gz file exists).

Example:

module main

import veb

pub struct Context {
    veb.Context
}

pub struct App {
    veb.StaticHandler
    veb.Middleware[Context]
}

pub fn (mut app App) index(mut ctx Context) veb.Result {
    return ctx.html('<h1>Compression demo</h1>
    <p>Visit <a href="/app.js">/app.js</a> or <a href="/style.css">/style.css</a>
    </p>')
}

fn main() {
    mut app := &App{}

    // Enable static file compression (zstd/gzip, disabled by default)
    // Use enable_static_zstd and enable_static_gzip for specific compression
    app.enable_static_compression = true
    // Maximum file size for auto-compression is 512 KB (default: 1MB)
    app.static_compression_max_size = 524288
    app.static_compression_mime_types = [veb.mime_types['.css'], veb.mime_types['.js']]

    // Serve files from the 'static' directory
    app.handle_static('static', true)!

    // Add the content encoding middleware to compress dynamic routes as well
    // This will use zstd if the client supports it, otherwise gzip
    // Use encode_gzip or encode_zstd for specific compression
    app.use(veb.encode_auto[Context]())

    veb.run[App, Context](mut app, 8080)
}

Set app.static_compression_mime_types when you only want to compress specific static MIME types. Leave it empty to keep the current behavior and allow compression for any static file type.

Setup and testing:

Create test files in the static directory:

mkdir -p static
echo "console.log('Hello from V web!');" > static/app.js
echo "body { margin: 0; }" > static/style.css
# Pre-compress style.css manually for zero-copy streaming (zstd or gzip)
zstd -k static/style.css  # creates style.css.zst
# or: gzip -k static/style.css  # creates style.css.gz

Run the server, it will listen on port 8080:

v run server.v

Test compression with cURL:

# Test zstd compression (preferred when client supports it)
curl -H "Accept-Encoding: zstd, gzip" -i http://localhost:8080/app.js
# Expected headers:
# Content-Encoding: zstd
# Vary: Accept-Encoding

# Test gzip fallback (when client doesn't support zstd)
curl -H "Accept-Encoding: gzip" -i http://localhost:8080/app.js
# Expected headers:
# Content-Encoding: gzip
# Vary: Accept-Encoding

# Request with automatic decompression
curl -H "Accept-Encoding: zstd, gzip" --compressed http://localhost:8080/app.js

# Request without encoding - should return uncompressed content
curl -i http://localhost:8080/app.js

# Auto-generated cache files are stored under:
# os.cache_dir()/veb/static_compression/

# Test manual pre-compression - style.css.zst is served directly (zero-copy)
curl -H "Accept-Encoding: zstd" -i http://localhost:8080/style.css

Performance tips:

  • For production, you can pre-compress your static files with zstd (zstd -k static/app.js) or gzip (gzip -k static/app.js) and veb will serve them directly without loading into memory.
  • Zstd offers better compression ratio and speed than gzip - use it when possible.

Priority order: When both .zst and .gz files exist for the same source file, veb will serve .zst if the client supports zstd, otherwise .gz if gzip is supported.

  • The lazy cache is created on first request, so the first visitor pays a small compression cost, but all subsequent requests are served at zero-copy speed.
  • Auto-generated cache files live in the OS cache directory, so read-only static folders still work and your source tree stays clean.
  • Large files (> threshold) are always streamed, ensuring low memory usage even for large assets.
  • The encode_auto middleware automatically chooses zstd or gzip based on client support. You can also use encode_zstd or encode_gzip for specific compression.
  • If caching fails (e.g., on read-only filesystems), veb automatically falls back to serving compressed content from memory. You can set static_compression_max_size = 0 to disable auto-compression completely. For optimal performance on read-only systems, pre-compress all files with zstd -k or gzip -k.

Markdown content negotiation

veb can provide automatic content negotiation for markdown files, allowing you to serve markdown content when the client explicitly requests it via the Accept header. This is compliant to llms.txt proposal and useful for documentations that can serve the same content in multiple formats, more efficiently to AI services using it.

How it works:

When enable_markdown_negotiation is enabled and a client sends Accept: text/markdown, veb will try to serve markdown variants in the following priority order:

  1. path.md - Direct markdown file
  2. path.html.md - HTML-flavored markdown (for content that can be rendered as both)
  3. path/index.html.md - Directory index in markdown format

Without the Accept: text/markdown header, files are served normally based on their actual extension. This ensures backward compatibility - direct access to .md files always works regardless of the setting.

Example:

module main

import veb

pub struct Context {
    veb.Context
}

pub struct App {
    veb.StaticHandler
}

fn main() {
    mut app := &App{}

    // Enable markdown content negotiation (disabled by default)
    app.enable_markdown_negotiation = true

    // Serve files from the 'docs' directory
    app.handle_static('docs', true)!

    veb.run[App, Context](mut app, 8080)
}

Setup and testing:

Create test files in the docs directory:

mkdir -p docs
echo "# API Documentation" > docs/api.md
echo "# User Guide" > docs/guide.html.md
echo "<h1>HTML Version</h1>" > docs/api.html

Run the server:

v run server.v

Test content negotiation with cURL:

# Request markdown version with content negotiation - serves api.md
curl -H "Accept: text/markdown" http://localhost:8080/api

# Direct access to .md file always works, regardless of Accept header
curl http://localhost:8080/api.md

# Direct access to .html file
curl http://localhost:8080/api.html

# Without Accept: text/markdown header - returns 404 since 'api' without extension doesn't exist
curl http://localhost:8080/api

Middleware

Middleware in web development is (loosely defined) a hidden layer that sits between what a user requests (the HTTP Request) and what a user sees (the HTTP Response). We can use this middleware layer to provide "hidden" functionality to our apps endpoints.

To use veb's middleware we have to embed veb.Middleware on our app struct and provide the type of which context struct should be used.

Example:

pub struct App {
    veb.Middleware[Context]
}

Use case

We could, for example, get the cookies for an HTTP request and check if the user has already accepted our cookie policy. Let's modify our Context struct to store whether the user has accepted our policy or not.

Example:

pub struct Context {
    veb.Context
pub mut:
    has_accepted_cookies bool
}

In veb middleware functions take a mut parameter with the type of your context struct and must return bool. We have full access to modify our Context struct!

Middleware handlers can also be bound methods, such as app.session_middleware. That lets middleware read or update fields on your app struct and reuse resources that live on it.

The return value indicates to veb whether it can continue or has to stop. If we send a response to the client in a middleware function veb has to stop, so we return false.

Example:

pub fn check_cookie_policy(mut ctx Context) bool {
    // get the cookie
    cookie_value := ctx.get_cookie('accepted_cookies') or { '' }
    // check if the cookie has been set
    if cookie_value == 'true' {
        ctx.has_accepted_cookies = true
    }
    // we don't send a response, so we must return true
    return true
}

We can check this value in an endpoint and return a different response.

Example:

@['/only-cookies']
pub fn (app &App) only_cookie_route(mut ctx Context) veb.Result {
    if ctx.has_accepted_cookies {
        return ctx.text('Welcome!')
    } else {
        return ctx.text('You must accept the cookie policy!')
    }
}

There is one thing left for our middleware to work: we have to register our only_cookie_route function as middleware for our app. We must do this after the app is created and before the app is started.

Example:

fn main() {
    mut app := &App{}

    // register middleware for all routes
    app.use(handler: check_cookie_policy)

    // Pass the App and context type and start the web server on port 8080
    veb.run[App, Context](mut app, 8080)
}

If your middleware needs access to app state or shared resources, register a bound method instead of a free function:

@[heap]
pub struct App {
    veb.Middleware[Context]
mut:
    request_count int
}

pub fn (mut app App) session_middleware(mut ctx Context) bool {
    app.request_count++
    ctx.res.header.add_custom('X-Request-Count', app.request_count.str()) or { return false }
    return true
}

fn main() {
    mut app := &App{}
    app.use(handler: app.session_middleware)
    app.route_use('/admin/:path...', handler: app.session_middleware)
    veb.run[App, Context](mut app, 8080)
}

Types of middleware

In the previous example we used so called "global" middleware. This type of middleware applies to every endpoint defined on our app struct; global. It is also possible to register middleware for only a certain route(s).

Example:

// register middleware only for the route '/auth'
app.route_use('/auth', handler: auth_middleware)
// register middleware only for the route '/documents/' with a parameter
// e.g. '/documents/5'
app.route_use('/documents/:id')
// register middleware with a parameter array. The middleware will be registered
// for all routes that start with '/user/' e.g. '/user/profile/update'
app.route_use('/user/:path...')
// register middleware only for selected HTTP methods on a route
app.route_use('/admin/auth', handler: auth_middleware, methods: [.get, .delete])

If methods is omitted, route middleware applies to all HTTP methods on that route.

CORS header allow-lists

veb.CorsOptions.allowed_headers names the CORS request headers that the application accepts, such as X-Token and Content-Type. Validation compares these names case-insensitively with the comma-separated Access-Control-Request-Headers value. The origins option validates the browser's Origin header separately. Normal browser headers such as User-Agent can therefore accompany a request when an application header allow-list is configured.

Evaluation moment

By default, the registered middleware functions are executed before a method on your app struct is called. You can also change this behaviour to execute middleware functions after a method on your app struct is called, but before the response is sent!

Example:

pub fn modify_headers(mut ctx Context) bool {
    // add Content-Language: 'en-US' header to each response
    ctx.res.header.add(.content_language, 'en-US')
    return true
}
app.use(handler: modify_headers, after: true)

When to use which type

You could use "before" middleware to check and modify the HTTP request and you could use "after" middleware to validate the HTTP response that will be sent or do some cleanup.

Anything you can do in "before" middleware, you can do in "after" middleware.

Evaluation order

veb will handle requests in the following order:

  1. Execute global "before" middleware
  2. Execute "before" middleware that matches the requested route
  3. Execute the endpoint handler on your app struct
  4. Execute global "after" middleware
  5. Execute "after" middleware that matches the requested route

In each step, except for step 3, veb will evaluate the middleware in the order that they are registered; when you call app.use or app.route_use.

Early exit

If any middleware sends a response (and thus must return false) veb will not execute any other middleware, or the endpoint method, and immediately send the response.

Example:

pub fn early_exit(mut ctx Context) bool {
    ctx.text('early exit')
    // we send a response from middleware, so we have to return false
    return false
}

pub fn logger(mut ctx Context) bool {
    println('received request for "${ctx.req.url}"')
    return true
}
app.use(handler: early_exit)
app.use(handler: logger)

Because we register early_exit before logger our logging middleware will never be executed!

Limiting concurrent requests

veb.throttle caps how many requests run their handlers at once. A request over the cap waits for a free slot; when max_waiting requests are already waiting, or the wait passes wait_timeout, it is refused with 503 Service Unavailable and a Retry-After header.

th := veb.throttle[Context](max_concurrent: 8, max_waiting: 16, wait_timeout: 2 * time.second)
// register it after middleware that refuses requests cheaply, such as authentication
app.use(th.acquire())
// or only for some routes; every registration of `th` shares the same cap
app.route_use('/reports/:path...', th.acquire())

The middleware takes a slot, and veb frees it when the request finishes, whichever way it finishes: after the handler and the "after" middleware, on a 404, or when a later middleware refuses the request. There is nothing to register for the release.

A waiting request sleeps on the thread that serves it. veb serves each connection inline on a fixed set of worker threads, so a waiting request also delays the other connections of its thread, and no more requests than there are worker threads run handlers at once. Keep max_concurrent below the worker count for the cap to matter, and use max_waiting: 0 to refuse at once instead of queueing.

Controllers

Controllers can be used to split up your app logic so you are able to have one struct per "route group". E.g. a struct Admin for urls starting with '/admin' and a struct Foo for urls starting with '/foo'.

To use controllers we have to embed veb.Controller on our app struct and when we register a controller we also have to specify what the type of the context struct will be. That means that it is possible to have a different context struct for each controller and the main app struct.

Example:

module main

import veb

pub struct Context {
    veb.Context
}

pub struct App {
    veb.Controller
}

// this endpoint will be available at '/'
pub fn (app &App) index(mut ctx Context) veb.Result {
    return ctx.text('from app')
}

pub struct Admin {}

// this endpoint will be available at '/admin/'
pub fn (app &Admin) index(mut ctx Context) veb.Result {
    return ctx.text('from admin')
}

pub struct Foo {}

// this endpoint will be available at '/foo/'
pub fn (app &Foo) index(mut ctx Context) veb.Result {
    return ctx.text('from foo')
}

fn main() {
    mut app := &App{}

    // register the controllers the same way as how we start a veb app
    mut admin_app := &Admin{}
    app.register_controller[Admin, Context]('/admin', mut admin_app)!

    mut foo_app := &Foo{}
    app.register_controller[Foo, Context]('/foo', mut foo_app)!

    veb.run[App, Context](mut app, 8080)
}

You can do everything with a controller struct as with a regular App struct. Register middleware, add static files and you can even register other controllers! Static assets registered on either the main app or a controller keep working when controllers are mounted, including controllers mounted at '/'.

Routing

Any route inside a controller struct is treated as a relative route to its controller namespace.

@['/path']
pub fn (app &Admin) path(mut ctx Context) veb.Result {
    return ctx.text('Admin')
}

When we registered the controller with app.register_controller[Admin, Context]('/admin', mut admin_app)! we told veb that the namespace of that controller is '/admin' so in this example we would see the text "Admin" if we navigate to the url '/admin/path'.

veb doesn't support duplicate routes, so if we add the following route to the example the code will produce an error.

@['/admin/path']
pub fn (app &App) admin_path(mut ctx Context) veb.Result {
    return ctx.text('Admin overwrite')
}

There will be an error, because the controller Admin handles all routes starting with '/admin': the endpoint admin_path is unreachable.

Controller with hostname

You can also set a host for a controller. All requests coming to that host will be handled by the controller.

Example:

struct Example {}

// You can only access this route at example.com: http://example.com/
pub fn (app &Example) index(mut ctx Context) veb.Result {
    return ctx.text('Example')
}
mut example_app := &Example{}
// set the controllers hostname to 'example.com' and handle all routes starting with '/',
// we handle requests with any route to 'example.com'
app.register_host_controller[Example, Context]('example.com', '/', mut example_app)!

Context Methods

veb has a number of utility methods that make it easier to handle requests and send responses. These methods are available on veb.Context and directly on your own context struct if you embed veb.Context. Below are some of the most used methods, look at the standard library documentation to see them all.

Request methods

You can directly access the HTTP request on the .req field.

Get the client IP address

ctx.ip() returns the socket peer's IP address by default. Forwarding headers from direct clients are ignored. To accept them from your reverse proxies, declare their IP addresses on the app:

pub struct App {
    trusted_proxies []string = ['10.0.0.1', '10.0.0.2']
}

Only list proxies you control and configure to append the actual peer to X-Forwarded-For. Addresses must be individual IPv4 or IPv6 literals; CIDR ranges are not supported. When the socket peer is trusted, ip() combines all X-Forwarded-For fields in their received order and walks the chain from right to left, stopping at the nearest untrusted address. For example, trusted peer 10.0.0.2 with 203.0.113.66, 198.51.100.9, 10.0.0.1 returns 198.51.100.9: an earlier prefix supplied by that client cannot choose its identity. If every hop is trusted, the left-most address is returned. Empty or malformed entries stop the walk at the last trusted hop, and IPv6 addresses are returned in canonical form.

Only when X-Forwarded-For is absent may a trusted peer supply CF-Connecting-IP, followed by X-Real-Ip. These must contain a single valid IP address. Configure trusted proxies to overwrite or remove these headers from untrusted clients, since they carry no proxy chain. An empty or malformed X-Forwarded-For never falls back to these headers.

Get request headers

Example:

pub fn (app &App) index(mut ctx Context) veb.Result {
    content_length := ctx.get_header(.content_length) or { '0' }
    // get custom header
    custom_header := ctx.get_custom_header('X-HEADER') or { '' }
    // ...
}

A request can have up to http.max_headers (50) header fields. veb answers a request that has more with 431 Request Header Fields Too Large, and closes the connection.

Example:

pub fn (app &App) index(mut ctx Context) veb.Result {
    cookie_val := ctx.get_cookie('token') or { '' }
    // ...
}

Response methods

You can directly modify the HTTP response by changing the res field, which is of the type http.Response.

Send response with different MIME types

// send response HTTP_OK with content-type `text/html`
ctx.html('<h1>Hello world!</h1>')
// send response HTTP_OK with content-type `text/plain`
ctx.text('Hello world!')
// stringify the object and send response HTTP_OK with content-type `application/json`
ctx.json(User{
    name: 'test'
    age: 20
})
// send response HTTP_NO_CONTENT (204) without a content-type and body
ctx.no_content()

Sending files

Example:

pub fn (app &App) file_response(mut ctx Context) veb.Result {
    // send the file 'image.png' in folder 'data' to the user
    return ctx.file('data/image.png')
}

Set response headers

Example:

pub fn (app &App) index(mut ctx Context) veb.Result {
    ctx.set_header(.accept, 'text/html')
    // set custom header
    ctx.set_custom_header('X-HEADER', 'my-value')!
    // ...
}

Example:

pub fn (app &App) index(mut ctx Context) veb.Result {
    ctx.set_cookie(http.Cookie{
        name: 'token'
        value: 'true'
        path: '/'
        secure: true
        http_only: true
    })
    // ...
}

Redirect

You must pass the type of redirect to veb:

  • moved_permanently HTTP code 301
  • found HTTP code 302
  • see_other HTTP code 303
  • temporary_redirect HTTP code 307
  • permanent_redirect HTTP code 308

Common use cases:

If you want to change the request method, for example when you receive a post request and want to redirect to another page via a GET request, you should use see_other. If you want the HTTP method to stay the same, you should use found generally speaking.

Example:

pub fn (app &App) index(mut ctx Context) veb.Result {
    token := ctx.get_cookie('token') or { '' }
    if token == '' {
        // redirect the user to '/login' if the 'token' cookie is not set
        // we explicitly tell the browser to send a GET request
        return ctx.redirect('/login', typ: .see_other)
    } else {
        return ctx.text('Welcome!')
    }
}

Sending error responses

Example:

pub fn (app &App) login(mut ctx Context) veb.Result {
    if username := ctx.form['username'] {
        return ctx.text('Hello "${username}"')
    } else {
        // send an HTTP 400 Bad Request response with a message
        return ctx.request_error('missing form value "username"')
    }
}

You can also use ctx.server_error(msg string) to send an HTTP 500 internal server error with a message.

If a handler panics, veb logs the panic and responds with HTTP 500 and the plain-text body Internal Server Error. This replaces any prepared body or file response and its content, framing, and cache metadata. After-middleware is skipped. A connection taken over with ctx.takeover_conn() or ctx.takeover_conn_reusable() remains under the handler's control; recovery does not write another response to it.

Handlers that return !veb.Result

A handler that can fail may return !veb.Result instead of rolling its own error responses. Answer by returning the value, decline by returning an error, and veb turns the error into a JSON envelope carrying its message. When the handler already chose a status with ctx.res.set_status, it is kept, so 4xx stays in the handler's hands; otherwise the envelope is a 500.

Example:

module main

import veb

pub struct Context {
    veb.Context
}

pub struct App {}

fn user_name_by_id(id int) !string {
    if id != 42 {
        return error('no such user')
    }
    return 'douglas'
}

@['/user/:id']
pub fn (app &App) user(mut ctx Context, id int) !veb.Result {
    // A status the handler chose is kept, so `return error(...)` here answers
    // 404 with a JSON envelope carrying the message.
    name := user_name_by_id(id) or {
        ctx.res.set_status(.not_found)
        return error('no user with id ${id}')
    }
    return ctx.text('Hello ${name}!')
}

Advanced usage

If you need more control over the TCP connection with a client, for example when you want to keep the connection open. You can call ctx.takeover_conn.

When this function is called you are free to do anything you want with the TCP connection and veb will not interfere. This means that we are responsible for sending a response over the connection and closing it.

Empty Result

Sometimes you want to send the response in another thread, for example when using Server Sent Events. When you are sure that a response will be sent over the TCP connection you can return veb.no_result(). This function does nothing and returns an empty veb.Result struct, letting veb know that we sent a response ourselves.

Note:
It is important to call ctx.takeover_conn before you spawn a thread

Example:

module main

import net
import time
import veb

pub struct Context {
    veb.Context
}

pub struct App {}

pub fn (app &App) index(mut ctx Context) veb.Result {
    return ctx.text('hello!')
}

@['/long']
pub fn (app &App) long_response(mut ctx Context) veb.Result {
    // let veb know that the connection should not be closed
    ctx.takeover_conn()
    // use spawn to handle the connection in another thread
    // if we don't the whole web server will block for 10 seconds,
    // since veb is singlethreaded
    spawn handle_connection(mut ctx.conn)
    // we will send a custom response ourselves, so we can safely return an empty result
    return veb.no_result()
}

fn handle_connection(mut conn net.TcpConn) {
    defer {
        conn.close() or {}
    }
    // block for 10 second
    time.sleep(time.second * 10)
    conn.write_string('HTTP/1.1 200 OK\r\nContent-type: text/html\r\nContent-length: 15\r\n\r\nHello takeover!') or {}
}

fn main() {
    mut app := &App{}
    veb.run[App, Context](mut app, 8080)
}

Constants #

const max_http_post_size = $d('veb_max_http_post_size_bytes', 1048576)
const default_port = int($d('veb_default_port', 8080))
const methods_with_form = [http.Method.post, .put, .patch]
const headers_close = http.new_custom_header_from_map({
	'Server': 'veb'
})!
const http_302 = http.new_response(
	status: .found
	body:   '302 Found'
	header: headers_close
)
const http_400 = http.new_response(
	status: .bad_request
	body:   '400 Bad Request'
	header: http.new_header(
		key:   .content_type
		value: 'text/plain'
	).join(headers_close)
)
const http_404 = http.new_response(
	status: .not_found
	body:   '404 Not Found'
	header: http.new_header(
		key:   .content_type
		value: 'text/plain'
	).join(headers_close)
)
const http_408 = http.new_response(
	status: .request_timeout
	body:   '408 Request Timeout'
	header: http.new_header(
		key:   .content_type
		value: 'text/plain'
	).join(headers_close)
)
const http_413 = http.new_response(
	status: .request_entity_too_large
	body:   '413 Request entity is too large'
	header: http.new_header(
		key:   .content_type
		value: 'text/plain'
	).join(headers_close)
)
const http_500 = http.new_response(
	status: .internal_server_error
	body:   '500 Internal Server Error'
	header: http.new_header(
		key:   .content_type
		value: 'text/plain'
	).join(headers_close)
)
const mime_types = {
	'.aac':    'audio/aac'
	'.abw':    'application/x-abiword'
	'.arc':    'application/x-freearc'
	'.avi':    'video/x-msvideo'
	'.azw':    'application/vnd.amazon.ebook'
	'.bin':    'application/octet-stream'
	'.bmp':    'image/bmp'
	'.bz':     'application/x-bzip'
	'.bz2':    'application/x-bzip2'
	'.cda':    'application/x-cdf'
	'.csh':    'application/x-csh'
	'.css':    'text/css'
	'.csv':    'text/csv'
	'.doc':    'application/msword'
	'.docx':   'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
	'.eot':    'application/vnd.ms-fontobject'
	'.epub':   'application/epub+zip'
	'.gz':     'application/gzip'
	'.gif':    'image/gif'
	'.htm':    'text/html'
	'.html':   'text/html'
	'.ico':    'image/vnd.microsoft.icon'
	'.ics':    'text/calendar'
	'.jar':    'application/java-archive'
	'.jpeg':   'image/jpeg'
	'.jpg':    'image/jpeg'
	'.js':     'text/javascript'
	'.json':   'application/json'
	'.jsonld': 'application/ld+json'
	'.md':     'text/markdown'
	'.mid':    'audio/midi audio/x-midi'
	'.midi':   'audio/midi audio/x-midi'
	'.mjs':    'text/javascript'
	'.mp3':    'audio/mpeg'
	'.mp4':    'video/mp4'
	'.mpeg':   'video/mpeg'
	'.mpkg':   'application/vnd.apple.installer+xml'
	'.odp':    'application/vnd.oasis.opendocument.presentation'
	'.ods':    'application/vnd.oasis.opendocument.spreadsheet'
	'.odt':    'application/vnd.oasis.opendocument.text'
	'.oga':    'audio/ogg'
	'.ogv':    'video/ogg'
	'.ogx':    'application/ogg'
	'.opus':   'audio/opus'
	'.otf':    'font/otf'
	'.png':    'image/png'
	'.pdf':    'application/pdf'
	'.php':    'application/x-httpd-php'
	'.ppt':    'application/vnd.ms-powerpoint'
	'.pptx':   'application/vnd.openxmlformats-officedocument.presentationml.presentation'
	'.rar':    'application/vnd.rar'
	'.rtf':    'application/rtf'
	'.scss':   'text/css'
	'.sh':     'application/x-sh'
	'.svg':    'image/svg+xml'
	'.swf':    'application/x-shockwave-flash'
	'.tar':    'application/x-tar'
	'.tif':    'image/tiff'
	'.tiff':   'image/tiff'
	'.ts':     'video/mp2t'
	'.ttf':    'font/ttf'
	'.txt':    'text/plain'
	'.vsd':    'application/vnd.visio'
	'.wasm':   'application/wasm'
	'.wav':    'audio/wav'
	'.weba':   'audio/webm'
	'.webm':   'video/webm'
	'.webp':   'image/webp'
	'.woff':   'font/woff'
	'.woff2':  'font/woff2'
	'.xhtml':  'application/xhtml+xml'
	'.xls':    'application/vnd.ms-excel'
	'.xlsx':   'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
	'.xml':    'application/xml'
	'.xul':    'application/vnd.mozilla.xul+xml'
	'.zip':    'application/zip'
	'.zst':    'application/zstd'
	'.3gp':    'video/3gpp'
	'.3g2':    'video/3gpp2'
	'.7z':     'application/x-7z-compressed'
	'.m3u8':   'application/vnd.apple.mpegurl'
	'.vsh':    'text/x-vlang'
	'.v':      'text/x-vlang'
}
const cors_safelisted_response_headers = [http.CommonHeader.cache_control, .content_language,
	.content_length, .content_type, .expires, .last_modified, .pragma].map(it.str()).join(',')

fn access_log #

fn access_log[T](options AccessLogOptions) AccessLog[T]

access_log builds the pair. Register both halves, globally or on the same route:

log := veb.access_log[Context]()
app.use(log.stamp())
app.use(log.finish())

fn body_limit #

fn body_limit[T](max_bytes int) MiddlewareOptions[T]

body_limit returns middleware that answers a request whose body has more than max_bytes bytes with a 413 that names the limit, before the handler runs. Register it with route_use on the routes that need a lower limit than the rest of the app: a JSON API may take 100KB while the avatar upload beside it takes 5MB.

app.route_use('/api/profile', veb.body_limit[Context](100 * 1024))

It is a per-route policy, not memory protection: before any middleware runs, veb has already read the whole body into memory and parsed its form or multipart data. The limit for that is max_request_body_size in RunParams (64 MiB by default), which answers a larger body with 413 and closes the connection. body_limit only has an effect when max_bytes is lower than max_request_body_size, and it does not close the connection.

max_bytes of 0 refuses any non-empty body. A negative max_bytes panics.

fn controller #

fn controller[A, X](path string, mut global_app A) !&ControllerPath

controller generates a new Controller for the main app

fn controller_host #

fn controller_host[A, X](host string, path string, mut global_app A) !&ControllerPath

controller_host generates a controller which only handles incoming requests from the host domain

fn cors #

fn cors[T](options CorsOptions) MiddlewareOptions[T]

cors handles cross-origin requests by adding Access-Control-* headers to a preflight request and validating the headers of a cross-origin request. Usage example:

app.use(veb.cors[Context](veb.CorsOptions{
    origins: ['*']
    allowed_methods: [.get, .head, .patch, .put, .post, .delete]
}))

fn decode_gzip #

fn decode_gzip[T]() MiddlewareOptions[T]

decode_gzip decodes the body of a gzip'ed HTTP request. Register this middleware before you do anything with the request body! Usage example: app.use(veb.decode_gzipContext)

fn decode_zstd #

fn decode_zstd[T]() MiddlewareOptions[T]

decode_zstd decodes the body of a zstd-compressed HTTP request. Register this middleware before you do anything with the request body! Usage example: app.use(veb.decode_zstdContext)

fn encode_auto #

fn encode_auto[T]() MiddlewareOptions[T]

encode_auto adds automatic content encoding (zstd or gzip) based on the client's Accept-Encoding header. This middleware checks the Accept-Encoding header and compresses with zstd if supported, otherwise gzip. Static files in streaming mode are compressed by send_file() when static compression is enabled, and this middleware skips them to avoid double compression (via the already_compressed flag). Register this middleware as last! Usage example: app.use(veb.encode_autoContext)

fn encode_gzip #

fn encode_gzip[T]() MiddlewareOptions[T]

encode_gzip adds gzip encoding to the HTTP Response body. This middleware compresses dynamic routes and static files loaded in memory (takeover mode). Static files in streaming mode are compressed by send_file() when static compression is enabled, and this middleware skips them to avoid double compression (via the already_compressed flag). Register this middleware as last! Usage example: app.use(veb.encode_gzipContext)

fn encode_zstd #

fn encode_zstd[T]() MiddlewareOptions[T]

encode_zstd adds zstd encoding to the HTTP Response body. This middleware compresses dynamic routes and static files loaded in memory (takeover mode). Static files in streaming mode are compressed by send_file() when static compression is enabled, and this middleware skips them to avoid double compression (via the already_compressed flag). Register this middleware as last! Usage example: app.route_use('/api', veb.encode_zstdContext)

fn filter_html #

fn filter_html[T](x T) string

filter_html renders a $veb.html() template interpolation value. A plain string is HTML-escaped so user-controlled data cannot inject markup; a veb.RawHtml value is emitted verbatim; any other value uses its default string representation. This mirrors v1, where cgen wraps only string-typed template interpolations with veb__filter (RawHtml has a distinct type there). The v3 backend lowers templates to source before types are known, so the escaping decision is made here at render time; the comptime string branch also matches string aliases, so the trusted type is matched by its exact veb.RawHtml name. Any other alias (e.g. a main.RawHtml holding user-controlled data) is still escaped rather than emitted verbatim.

fn format_access_log #

fn format_access_log(o AccessLogOptions, ctx &Context) string

format_access_log renders one response as a JSON log line. It is a pure function of the finished context so the format is testable without a server, a clock, or stdout.

fn new_openapi #

fn new_openapi(config OpenApiConfig) !OpenApiDocument

new_openapi creates a document, optionally reading its API version from a v.mod file.

fn no_result #

fn no_result() Result

no_result does nothing, but returns veb.Result. Only use it when you are sure a response will be send over the connection, or in combination with Context.takeover_conn

fn openapi_json #

fn openapi_json[A, X](app &A, title string, version string) !string

openapi_json renders the app's route table as an OpenAPI 3.0 document: one path entry per route, one operation per method, with path parameters listed. It reads the same generate_routes output the dispatcher reads, so the document cannot drift from the code — it is the code, printed.

Parameters are typed string: the route table records names, not declared kinds. Typed binding (ctx.bind[T]) will enrich this when it lands; until then a skeleton clients can generate against beats no document at all.

spec := veb.openapi_json[App, Context](app, 'Shop API', '1.0.0')!
os.write_file('openapi.json', spec)!

fn rate_limit #

fn rate_limit[T](options RateLimitOptions) MiddlewareOptions[T]

rate_limit returns middleware that caps requests per client key at the configured rate, answering 429 with a Retry-After header past it. New clients also receive 429 while all max_keys buckets are occupied. Register it globally or on the routes that need it:

app.use(veb.rate_limit[Context](requests_per_second: 5, burst: 10))

fn raw #

fn raw(s string) RawHtml

fn render_request #

fn render_request[A, X](mut app A, req http.Request) !&Context

render_request dispatches one request without starting a listener and returns the completed base context. Read the response from ctx.res; fields added to the application's context type are only available inside its handlers. Static file responses retain return_type and return_file, as on the server. X must embed veb.Context. Context, route, and controller configuration errors are returned to the caller.

fn route_info #

fn route_info[A, X](app &A) ![]RouteInfo

route_info returns root and recursively registered controller routes from the dispatcher metadata.

fn run #

fn run[A, X](mut global_app A, port int)

run - start a new veb server, listening to all available addresses, at the specified port

fn run_at #

fn run_at[A, X](mut global_app A, params RunParams) !

fn run_new #

fn run_new[A, X](mut global_app A, params RunParams) !

run_new - start a new veb server using the parallel fasthttp backend.

fn secure_headers #

fn secure_headers[T](options SecureHeadersOptions) MiddlewareOptions[T]

secure_headers returns middleware that sets security headers on every response, then lets the request continue. Register it before any route that serves a browser, and before compression so the headers travel with the compressed body. Usage example:

app.use(veb.secure_headers[Context]())

fn throttle #

fn throttle[T](options ThrottleOptions) Throttle[T]

throttle builds a throttle. Register its middleware globally or on routes, after any middleware that refuses requests cheaply, such as authentication, so refused requests never wait for a slot:

th := veb.throttle[Context](max_concurrent: 8)
app.use(th.acquire())

There is no release half to register: veb frees the slot when the request finishes, whichever way it finishes.

fn tr #

fn tr(lang string, key string) string

Used by %key in templates

fn tr_plural #

fn tr_plural(lang string, key string, amount int) string

fn url_for #

fn url_for[A](handler string, params ...string) ?string

url_for builds the URL for one of the app's handlers from its method name and the route parameters in the order the route declares them, so renaming a handler or reordering its parameters breaks the call site at compile time instead of shipping a 404. An unknown handler, or a parameter count that does not fit the route, is none rather than a half-built path.

@['/user/:id']
pub fn (app &App) user(mut ctx Context, id int) veb.Result { ... }

url := veb.url_for[App]('user', '42') or { '' } // '/user/42'

fn RedirectType.from #

fn RedirectType.from[W](input W) !RedirectType

from returns the RedirectType value of an integer or of a field name, or an error if there is none.

interface StaticApp #

interface StaticApp {
mut:
	static_files                  map[string]string
	static_mime_types             map[string]string
	static_hosts                  map[string]string
	static_prefixes               []string
	enable_static_gzip            bool
	enable_static_zstd            bool
	enable_static_compression     bool
	static_compression_max_size   int
	static_compression_mime_types []string
	enable_markdown_negotiation   bool
}

fn (AccessLog[T]) stamp #

fn (l &AccessLog[T]) stamp() MiddlewareOptions[T]

stamp records the arrival time on the request context.

fn (AccessLog[T]) finish #

fn (l &AccessLog[T]) finish() MiddlewareOptions[T]

finish writes one JSON line for the response the handler produced, then lets it through. A request the stamp half never saw still logs; its latency is -1, which is honest about what is unknown. The handler keeps its own copy of the options, so the pair may be built in a helper function that returns before the server starts.

type MiddlewareHandler #

type MiddlewareHandler[T] = fn (mut T) bool

fn (Middleware[T]) str #

fn (m &Middleware[T]) str() string

string representation of Middleware

fn (Middleware[T]) use #

fn (mut m Middleware[T]) use(options MiddlewareOptions[T])

use registers a global middleware handler

fn (Middleware[T]) route_use #

fn (mut m Middleware[T]) route_use(route string, options MiddlewareOptions[T])

route_use registers a middleware handler for a specific route(s)

type OpenApiSchema #

type OpenApiSchema = map[string]json2.Any

OpenApiSchema is an OpenAPI Schema Object. Custom schemas may use any OpenAPI keywords.

type RawHtml #

type RawHtml = string

A type which doesn't get filtered inside templates

fn (Throttle[T]) acquire #

fn (t Throttle[T]) acquire() MiddlewareOptions[T]

acquire returns the middleware that takes a handler slot before the handler runs. A request that cannot start promptly is refused with 503 and Retry-After instead. The middleware keeps its own copy of the throttle, so the Throttle may be a local of a setup function that returns before the first request arrives.

enum RedirectType #

enum RedirectType {
	found              = int(http.Status.found)
	moved_permanently  = int(http.Status.moved_permanently)
	see_other          = int(http.Status.see_other)
	temporary_redirect = int(http.Status.temporary_redirect)
	permanent_redirect = int(http.Status.permanent_redirect)
}

struct AccessLog #

struct AccessLog[T] {
	options AccessLogOptions
	// write receives every finished line; tests in this module capture it.
	write fn (line string) = print_access_log_line
}

AccessLog is the stamp half and the log half of one access log sharing nothing but the request context: veb middleware runs before a handler or after it, never around it.

struct AccessLogOptions #

@[params]
struct AccessLogOptions {
pub:
	// include_query decides whether the logged path keeps its query string.
	// Query strings often carry tokens and secrets; the default drops them.
	include_query bool
	// request_id_header is the response header the request id is read from.
	// Keep it equal to `request_id.Config.header` when that one is changed.
	request_id_header string = 'X-Request-ID'
}

AccessLogOptions configures the access_log middleware: one JSON line on stdout for each response that reaches veb's after-middleware, carrying method, path, status, bytes, latency and the request id when one is present. Standard output is the destination on purpose — collectors, files and rotation belong outside the process.

veb runs no after-middleware for a request that matched no route (the 404 the router answers with) or whose response a before-middleware sent by returning false (a 401 from an auth check, a CORS rejection), so those responses are not logged.

struct Context #

@[heap]
struct Context {
mut:
	// veb will try to infer the content type base on file extension,
	// and if `content_type` is not empty the `Content-Type` header will always be
	// set to this value
	content_type string
	// done is set to true when a response can be sent over `conn`
	done bool
	// If the `Connection: close` header is present the connection should always be closed
	client_wants_to_close bool
	// Configuration for static file compression (set by serve_if_static)
	enable_static_gzip            bool
	enable_static_zstd            bool
	enable_static_compression     bool
	static_compression_max_size   int
	static_compression_mime_types []string
	// controls whether veb should automatically send the response or whether the handler
	// takes over response writing.
	takeover_mode         ContextTakeoverMode
	return_file           string
	custom_mime_types_ref &map[string]string = unsafe { nil }
	// raw client file descriptor, used by the fasthttp backend to create a TcpConn
	// on demand when takeover_conn() is called
	client_fd int = -1
	// already_compressed indicates that the response body is already compressed (zstd/gzip)
	// and the compression middlewares should skip it
	already_compressed bool
	// throttle_leases are the handler slots throttle middleware took for this
	// request. `handle_route` frees them however the request ends: 404s and
	// requests a middleware refuses skip the after-middleware, so an
	// after-middleware could not free them reliably.
	throttle_leases []ThrottleLease
	// trusted_proxies are the socket peer addresses whose forwarding headers
	// `ip()` may believe. The framework copies this from a `trusted_proxies`
	// field on the app struct into every request context; an app that does not
	// declare one gets the empty list, so `ip()` answers the socket peer.
	trusted_proxies []string
	// access_log_start_ns is the monotonic clock reading (`time.sys_mono_now()`)
	// the access-log middleware stamped when the request arrived, so its finish
	// half can log the latency. Zero means unstamped.
	access_log_start_ns i64
pub:
	// TODO: move this to `handle_request`
	// time.ticks() from start of veb connection handle.
	// You can use it to determine how much time is spent on your request.
	page_gen_start i64
pub mut:
	// how the http response should be handled by veb's backend
	return_type       ContextReturnType = .normal
	req               http.Request @[skip]
	custom_mime_types map[string]string
	// TCP connection to client. Only for advanced usage!
	conn &net.TcpConn = unsafe { nil }
	// Map containing query params for the route.
	// http://localhost:3000/index?q=vpm&order_by=desc => { 'q': 'vpm', 'order_by': 'desc' }
	query map[string]string
	// Multipart-form fields.
	form map[string]string
	// Files from multipart-form.
	files map[string][]http.FileData
	res   http.Response
	// use form_error to pass errors from the context to your frontend
	form_error string
	// livereload_poll_interval_ms is not used: veb serves the livereload script
	// before any user code runs, so it could never be set. With
	// `-d veb_livereload_polling`, use `-d veb_livereload_poll_ms=<ms>` instead.
	livereload_poll_interval_ms int = 250 @[deprecated: 'use `-d veb_livereload_poll_ms=<ms>`']
}

The Context struct represents the Context which holds the HTTP request and response. It has fields for the query, form, files and methods for handling the request and response

fn (Context) bind #

fn (ctx &Context) bind[T]() !T

bind decodes the request into a struct of type T and validates it, returning the filled value. A JSON body (by content type) decodes into the full type, nested values included; anything else fills the scalar string, int and bool fields from the query and the form, form winning on conflict. A body that cannot fill the type, and a value that breaks a rule, is an error naming the field — never a silently coerced zero.

Validation reads struct attributes. Only this vocabulary is understood, and everything else — sql:, json: and whatever other layers annotate the same struct with — is ignored, so one struct can serve the table, the wire and the form at once:- required: the input must contain the key, and a string must be non-empty.- min: N / max: N: string length bounds, integer value bounds.

  • email: the string looks like local@domain.tld. An attribute outside this vocabulary that looks like validation is still ignored; misspelling a rule fails safe in the other direction, toward an unchecked field rather than a rejected request, so spell them right.
struct SignupInput {
    name     string @[required; max: 64]
    email    string @[required; email]
    password string @[required; min: 12]
    age      int    @[min: 13]
}

@['/signup'; post]
pub fn (app &App) signup(mut ctx Context) veb.Result {
    input := ctx.bind[SignupInput]() or {
        return ctx.request_error(err.msg())
    }
    return ctx.json(input)
}

fn (Context) get_header #

fn (ctx &Context) get_header(key http.CommonHeader) ?string

returns the request header data from the key

fn (Context) get_custom_header #

fn (ctx &Context) get_custom_header(key string) ?string

returns the request header data from the key

fn (Context) set_header #

fn (mut ctx Context) set_header(key http.CommonHeader, value string)

set a header on the response object

fn (Context) set_custom_header #

fn (mut ctx Context) set_custom_header(key string, value string) !

set a custom header on the response object

fn (Context) send_response_to_client #

fn (mut ctx Context) send_response_to_client(mimetype string, response string) Result

send_response_to_client finalizes the response headers and sets Content-Type to mimetype and the response body to response

fn (Context) html #

fn (mut ctx Context) html(s string) Result

Response with payload and content-type text/html

fn (Context) text #

fn (mut ctx Context) text(s string) Result

Response with s as payload and content-type text/plain

fn (Context) json #

fn (mut ctx Context) json[T](j T) Result

Response with json_s as payload and content-type application/json

fn (Context) json_pretty #

fn (mut ctx Context) json_pretty[T](j T) Result

Response with a pretty-printed JSON result

fn (Context) file #

fn (mut ctx Context) file(file_path string) Result

fn (Context) ok #

fn (mut ctx Context) ok(s string) Result

Response HTTP_OK with s as payload

fn (Context) request_error #

fn (mut ctx Context) request_error(msg string) Result

send an error 400 with a message

fn (Context) server_error #

fn (mut ctx Context) server_error(msg string) Result

send an error 500 with a message

fn (Context) error_with #

fn (mut ctx Context) error_with(err IError) Result

error_with answers a handler that declined to respond and returned an error instead, which is what a !veb.Result handler does. The response is a JSON envelope carrying the message. When the handler already chose a status with ctx.res.set_status, it is kept, so such handlers still control 4xx themselves; otherwise this is a 500.

fn (Context) server_error_with_status #

fn (mut ctx Context) server_error_with_status(s http.Status) Result

send an error with a custom status

fn (Context) no_content #

fn (mut ctx Context) no_content() Result

send a 204 No Content response without body and content-type

fn (Context) redirect #

fn (mut ctx Context) redirect(url string, params RedirectParams) Result

Redirect to an url

fn (Context) before_request #

fn (mut ctx Context) before_request()

before_request is always the first function that is executed. It can be overriden on your custom context. You can use it to log information about the current request, like its target url, client IP etc, or to enrich the request with common information, by setting session cookies, adding custom headers etc. For more control over the request, use the explicit Middleware support described in https://modules.vlang.io/veb.html#middleware .

fn (Context) not_found #

fn (mut ctx Context) not_found() Result

returns a HTTP 404 response

fn (Context) method_not_allowed #

fn (mut ctx Context) method_not_allowed(allowed []http.Method) Result

method_not_allowed returns a HTTP 405 response, listing the methods the matched route does accept in the Allow header.

fn (Context) set_content_type #

fn (mut ctx Context) set_content_type(mime string)

set_content_type sets the Content-Type header to mime

fn (Context) takeover_conn #

fn (mut ctx Context) takeover_conn()

takeover_conn prevents veb from automatically sending a response and closing the connection. You are responsible for closing the connection. In takeover mode if you call a Context method the response will be directly send over the connection and you can send multiple responses. This function is useful when you want to keep the connection alive and/or send multiple responses. Like with the SSE.

fn (Context) takeover_conn_reusable #

fn (mut ctx Context) takeover_conn_reusable()

takeover_conn_reusable prevents veb from automatically sending a response, but lets veb keep the connection in the read loop after the handler returns. The handler must write exactly one complete HTTP response with a clear body boundary, such as Content-Length or a final chunk for Transfer-Encoding: chunked. If the client asked to close the connection, veb will still close it.

fn (Context) user_agent #

fn (ctx &Context) user_agent() string

user_agent returns the user-agent header for the current client

fn (Context) ip #

fn (ctx &Context) ip() string

ip returns the client's IP address, defaulting to the socket peer. Forwarding headers are honoured only when the peer is in the app's trusted_proxies []string field. X-Forwarded-For is walked right to left until the nearest untrusted address. Only when it is absent are CF-Connecting-IP and then X-Real-Ip used; trusted proxies must overwrite or remove these single-address headers received from untrusted clients.

fn (Context) time_to_render #

fn (ctx &Context) time_to_render() i64

time_to_render returns the time in milliseconds that it took to render the page

fn (Context) error #

fn (mut ctx Context) error(s string)

Set s to the form error

struct Controller #

struct Controller {
pub mut:
	controllers []&ControllerPath
}

fn (Controller) register_controller #

fn (mut c Controller) register_controller[A, X](path string, mut global_app A) !

register_controller adds a new Controller to your app

fn (Controller) register_host_controller #

fn (mut c Controller) register_host_controller[A, X](host string, path string, mut global_app A) !

register_controller adds a new Controller to your app

struct ControllerPath #

struct ControllerPath {
pub:
	path    string
	handler ControllerHandler = unsafe { nil }
pub mut:
	host string
mut:
	// Retain metadata when the concrete controller type is erased by its handler.
	documented_routes []RouteInfo
}

struct CorsOptions #

@[params]
struct CorsOptions {
pub:
	// from which origin(s) can cross-origin requests be made; `Access-Control-Allow-Origin`
	origins []string @[required]
	// indicate whether the server allows credentials, e.g. cookies, in cross-origin requests.
	// ;`Access-Control-Allow-Credentials`
	allow_credentials bool
	// allowed CORS request header names; `Access-Control-Allow-Headers`.
	// Compared case-insensitively with `Access-Control-Request-Headers`.
	allowed_headers []string
	// allowed HTTP methods for a cross-origin request; `Access-Control-Allow-Methods`
	allowed_methods []http.Method
	// indicate if clients are able to access other headers than the "CORS-safelisted"
	// response headers; `Access-Control-Expose-Headers`
	expose_headers []string
	// how long the results of a preflight request can be cached, value is in seconds
	// ; `Access-Control-Max-Age`
	max_age ?int
}

CorsOptions is used to set CORS response headers. See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#the_http_response_headers

fn (CorsOptions) set_headers #

fn (options &CorsOptions) set_headers(mut ctx Context)

set_headers adds the CORS headers on the response

fn (CorsOptions) validate_request #

fn (options &CorsOptions) validate_request(mut ctx Context) bool

validate_request checks if a cross-origin request is made and verifies the CORS headers. If a cross-origin request is invalid this method will send a response using ctx. Header allow-lists apply to names advertised in Access-Control-Request-Headers.

struct Middleware #

struct Middleware[T] {
mut:
	global_handlers       []voidptr
	global_handlers_after []voidptr
	route_handlers        []RouteMiddleware
	route_handlers_after  []RouteMiddleware
}

struct MiddlewareOptions #

@[params]
struct MiddlewareOptions[T] {
pub:
	handler fn (mut ctx T) bool @[required]
	after   bool
	methods []http.Method
}

struct OpenApiConfig #

@[params]
struct OpenApiConfig {
pub:
	title               string
	version             string = '1.0.0'
	version_file        string
	description         string
	openapi_version     string = '3.0.3'
	nullable_options    bool
	schema_names        map[string]string
	security_schemes    map[string]json2.Any
	security            []map[string][]string
	controller_security map[string][]map[string][]string
	// Each status maps to a complete Response Object (description, content, headers, ...).
	default_errors        bool = true
	validation_response   bool
	security_errors       bool = true
	error_responses       map[string]json2.Any
	response_descriptions map[string]string
	servers               []map[string]json2.Any
	tags                  []map[string]json2.Any
	// Wrap successful typed responses in an object with this property, if nonempty.
	success_field string
}

OpenApiConfig configures an additive, typed OpenAPI document.

struct OpenApiDocument #

struct OpenApiDocument {
	config OpenApiConfig
mut:
	paths             map[string]map[string]map[string]json2.Any
	schemas           map[string]json2.Any
	success_overrides map[string]bool
}

OpenApiDocument collects dispatcher routes and explicitly configured DTO metadata.

fn (OpenApiDocument) add_app #

fn (mut doc OpenApiDocument) add_app[A, X](app &A) !

add_app includes root routes and all recursively mounted controllers.

fn (OpenApiDocument) operation #

fn (mut doc OpenApiDocument) operation(path string, method http.Method, options OpenApiOperation) !

operation applies structured metadata to an existing operation.

fn (OpenApiDocument) request #

fn (mut doc OpenApiDocument) request[T](path string, method http.Method, options OpenApiRequest) !

request adds a reflected JSON body or query parameters to an existing operation. Call twice with explicit query values to document both a body and query fields.

fn (OpenApiDocument) response #

fn (mut doc OpenApiDocument) response[T](path string, method http.Method, options OpenApiResponse) !

response adds a typed response; only successful responses inherit success_field wrapping.

fn (OpenApiDocument) raw_response #

fn (mut doc OpenApiDocument) raw_response(path string, method http.Method, status int, response map[string]json2.Any) !

raw_response adds a complete Response Object, including custom headers and error envelopes.

fn (OpenApiDocument) json #

fn (doc &OpenApiDocument) json(pretty bool) !string

json renders deterministic JSON and rejects duplicate operation IDs or undefined security schemes.

fn (OpenApiDocument) write #

fn (doc &OpenApiDocument) write(path string, pretty bool) !

write saves a rendered document to disk.

fn (OpenApiDocument) serve #

fn (doc &OpenApiDocument) serve(mut ctx Context, pretty bool) !Result

serve sends the specification from a veb handler with the JSON content type.

fn (OpenApiDocument) schema #

fn (mut doc OpenApiDocument) schema[T]() OpenApiSchema

schema registers T and its referenced types and returns its OpenAPI schema.

fn (OpenApiDocument) custom_schema #

fn (mut doc OpenApiDocument) custom_schema(name string, schema OpenApiSchema)

custom_schema registers a named schema, overriding reflection for that component name.

struct OpenApiOperation #

@[params]
struct OpenApiOperation {
pub:
	summary      string
	description  string
	operation_id string
	tags         []string
	// none inherits controller/document security; an empty array explicitly disables it.
	security     ?[]map[string][]string
	path_schemas map[string]OpenApiSchema
	parameters   []map[string]json2.Any
}

OpenApiOperation configures one handler without parsing source comments.

struct OpenApiRequest #

@[params]
struct OpenApiRequest {
pub:
	// none selects query for GET/DELETE/HEAD and a body for other methods.
	query       ?bool
	media_type  string = 'application/json'
	required    bool   = true
	description string
}

OpenApiRequest configures the location and media type of a typed request DTO.

struct OpenApiResponse #

@[params]
struct OpenApiResponse {
pub:
	status      int = 200
	description string
	media_type  string = 'application/json'
	// none inherits success_field for 2xx only; an empty string disables wrapping.
	envelope ?string
}

OpenApiResponse configures the status, media type and description of a typed response.

struct RateLimitOptions #

@[params]
struct RateLimitOptions {
pub:
	requests_per_second f64           = 10
	burst               int           = 20
	max_keys            int           = 10000
	idle_timeout        time.Duration = time.hour
	message             string        = '429 Too Many Requests'
}

RateLimitOptions configures the rate_limit middleware: a token bucket per client key, refilled at requests_per_second up to burst tokens. The key is the request IP. Once the table reaches max_keys, buckets idle longer than idle_timeout are swept first. If no slot is available, new clients receive 429 without evicting existing buckets. A nonpositive max_keys rejects every request without allocating buckets. See rate_limit.md for capacity and retry behavior.

struct RedirectParams #

@[params]
struct RedirectParams {
pub:
	typ RedirectType
}

struct Result #

@[noinit]
struct Result {}

A dummy structure that returns from routes to indicate that you actually sent something to a user

struct RouteInfo #

struct RouteInfo {
pub:
	name               string
	controller         string
	path               string
	methods            []http.Method
	host               string
	parameter_names    []string
	parameter_types    []string
	parameter_schemas  []map[string]json2.Any
	parameter_required []bool
}

RouteInfo describes a dispatched route, including its mounted controller path.

struct RunParams #

@[params]
struct RunParams {
pub:
	// use `family: .ip, host: 'localhost'` when you want it to bind only to 127.0.0.1
	family                    net.AddrFamily = .ip6
	host                      string
	port                      int  = default_port
	nr_workers                int  = 1
	show_startup_message      bool = true
	timeout_in_seconds        int  = 30
	max_request_buffer_size   int  = 8192
	benchmark_page_generation bool // for the "page rendered in X ms"
	ssl_config                mbedtls.SSLConnectConfig
	// max_request_body_size is the largest request body, in bytes, that the server accepts
	// (64 MiB by default). It answers a request that has a larger body with `413`, and
	// closes the connection. 0 removes the limit: a body is kept in memory, so use it only
	// where every client is trusted. A negative value is an error.
	max_request_body_size int = default_max_request_body_size
}

struct SecureHeadersOptions #

@[params]
struct SecureHeadersOptions {
pub:
	// content_security_policy is the `Content-Security-Policy` value. The
	// default allows only resources from the same origin.
	content_security_policy string = "default-src 'self'"
	// strict_transport_security is the `Strict-Transport-Security` value. It
	// only takes effect over HTTPS; disable it while developing over plain
	// HTTP if the browser has pinned an earlier value.
	strict_transport_security string = 'max-age=31536000; includeSubDomains'
	// x_frame_options is the `X-Frame-Options` value.
	x_frame_options string = 'SAMEORIGIN'
	// x_content_type_options is the `X-Content-Type-Options` value.
	x_content_type_options string = 'nosniff'
	// referrer_policy is the `Referrer-Policy` value.
	referrer_policy string = 'strict-origin-when-cross-origin'
}

SecureHeadersOptions configures the response headers the secure_headers middleware sets on every response. Every field ships with a safe default, and setting a field to '' disables that header. The defaults follow what Echo's Secure middleware and Fiber's Helmet integration send; the one deliberate omission is X-XSS-Protection, which modern guidance says not to send because content sniffing is already off and the header can re-enable attacks in old browsers that still honour it.

fn (SecureHeadersOptions) set_headers #

fn (options &SecureHeadersOptions) set_headers(mut ctx Context)

set_headers writes the configured security headers onto the response. It replaces any value already set, so a handler that sets e.g. its own Content-Security-Policy keeps it and the middleware only fills the rest.

struct Server #

@[heap]
struct Server {
	handle            fasthttp.ServerHandle
	lifecycle_control bool
}

fn (Server) wait_till_running #

fn (s &Server) wait_till_running(params WaitTillRunningParams) !int

wait_till_running waits until the server starts accepting requests.

fn (Server) shutdown #

fn (s &Server) shutdown(params ShutdownParams) !

shutdown gracefully stops accepting new requests and waits for in-flight requests to finish.

struct ShutdownParams #

@[params]
struct ShutdownParams {
pub:
	timeout         time.Duration = time.infinite
	retry_period_ms int           = 10
}

ShutdownParams configures how graceful shutdown waits for in-flight requests.

struct StaticHandler #

struct StaticHandler {
pub mut:
	static_files      map[string]string
	static_mime_types map[string]string
	static_hosts      map[string]string
	static_prefixes   []string
	// enable_static_gzip enables gzip compression for static files.
	// Use this for gzip-only compression. For automatic zstd/gzip selection, use enable_static_compression.
	// Default: false
	enable_static_gzip bool
	// enable_static_zstd enables zstd compression for static files.
	// Use this for zstd-only compression. For automatic zstd/gzip selection, use enable_static_compression.
	// Default: false
	enable_static_zstd bool
	// enable_static_compression enables automatic compression (zstd/gzip) for static files.
	// When enabled, Veb will choose zstd over gzip when client supports both (better compression ratio).
	// For gzip-only or zstd-only compression, use enable_static_gzip or enable_static_zstd instead.
	// Default: false
	enable_static_compression bool
	// static_compression_max_size sets the maximum file size in bytes for auto-compression.
	// Files larger than this threshold will not be auto-compressed.
	// Manual `.zst`/`.gz` files are still served for allowed MIME types.
	// Default: 1MB (1024*1024 bytes). Set to 0 to disable auto-compression completely.
	// Auto-generated cache files are stored in os.cache_dir()/veb/static_compression/.
	// If that cache directory is not writable, compressed content is served from memory as fallback.
	static_compression_max_size int = 1048576
	// static_compression_mime_types limits static compression to the listed MIME types.
	// Manual `.zst`/`.gz` files and auto-generated compressed cache files are only used for matching types.
	// Leave empty to preserve the default behavior and allow compression for all static MIME types.
	static_compression_mime_types []string
	// enable_markdown_negotiation allows the client sends Accept: text/markdown, then the server will serve .md files, if any.
	// Default: false (for backward compatibility)
	enable_markdown_negotiation bool
}

StaticHandler provides methods to handle static files in your veb App

fn (StaticHandler) handle_static #

fn (mut sh StaticHandler) handle_static(directory_path string, root bool) !bool

handle_static is used to mark a folder (relative to the current working folder) as one that contains only static resources (css files, images etc). If root is set the mount path for the dir will be in '/' Usage:

os.chdir( os.executable() )?
app.handle_static('assets', true)

fn (StaticHandler) host_handle_static #

fn (mut sh StaticHandler) host_handle_static(host string, directory_path string, root bool) !bool

host_handle_static is used to mark a folder (relative to the current working folder) as one that contains only static resources (css files, images etc). If root is set the mount path for the dir will be in '/' Usage:

os.chdir( os.executable() )?
app.host_handle_static('localhost', 'assets', true)

fn (StaticHandler) mount_static_folder_at #

fn (mut sh StaticHandler) mount_static_folder_at(directory_path string, mount_path string) !bool

mount_static_folder_at - makes all static files in directory_path and inside it, available at http://server/mount_path For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'), and you have a file /var/share/myassets/main.css . => That file will be available at URL: http://server/assets/main.css .

fn (StaticHandler) host_mount_static_folder_at #

fn (mut sh StaticHandler) host_mount_static_folder_at(host string, directory_path string, mount_path string) !bool

host_mount_static_folder_at - makes all static files in directory_path and inside it, available at http://host/mount_path For example: suppose you have called .host_mount_static_folder_at('localhost', '/var/share/myassets', '/assets'), and you have a file /var/share/myassets/main.css . => That file will be available at URL: http://localhost/assets/main.css .

fn (StaticHandler) serve_static #

fn (mut sh StaticHandler) serve_static(url string, file_path string) !

Serves a file static url is the access path on the site, file_path is the real path to the file, mime_type is the file type

fn (StaticHandler) host_serve_static #

fn (mut sh StaticHandler) host_serve_static(host string, url string, file_path string) !

Serves a file static url is the access path on the site, file_path is the real path to the file host is the host to serve the file from

struct Throttle #

struct Throttle[T] {
mut:
	store   &ThrottleStore
	options ThrottleOptions
	wait_ms i64
}

Throttle is one cap on concurrent handlers: a slot store and the middleware that takes slots from it. Every registration of the same throttle's middleware shares the one cap.

struct ThrottleOptions #

@[params]
struct ThrottleOptions {
pub:
	max_concurrent int           = 100
	max_waiting    int           = 100
	wait_timeout   time.Duration = 30 * time.second
	lease_timeout  time.Duration = time.minute
	message        string        = '503 Service Unavailable'
}

ThrottleOptions configures the throttle middleware: at most max_concurrent requests run their handlers at once, and at most max_waiting further requests queue for a slot, waiting up to wait_timeout each. Past either bound the middleware answers 503 with a Retry-After header instead of letting the pileup reach the handlers.

A rate limiter caps how often clients may arrive; a throttle caps how many may be inside at once.

A queued request waits by sleeping on the thread that serves it. veb's fasthttp backend serves each connection inline on a fixed set of worker threads, so a waiting request also stalls the other connections of its thread, and no more requests than there are worker threads run handlers at once. Keep max_concurrent below the worker count for the cap to bind, and set max_waiting: 0 to refuse at once instead of queueing.

veb frees a slot when its request finishes, however it finishes. lease_timeout is only a backstop: a slot held longer is reclaimed, so a handler still running past it over-admits rather than blocks everyone. Set it above your slowest handler, or to 0 to never reclaim a slot early.

struct WaitTillRunningParams #

@[params]
struct WaitTillRunningParams {
pub:
	max_retries     int = 100
	retry_period_ms int = 10
}

WaitTillRunningParams configures how long the server waits to report that it is serving.