Skip to content

Configuration

Introduction

Goyave provides a flexible structured configuration system that works with files but also supports environment variables. Most of the framework's core is configurable. Any other package can register their own configuration entries as well.

To configure your application, use the config.json file at your project's root. If the configuration file is missing some entries, the default values will be used. See the configuration reference below to know more.

WARNING

Configuration can be changed temporarily at runtime, but the operation is not concurrently safe. It is therefore advised to not use Config.Set() outside of tests.

Changing configuration at runtime is a rare use-case, so the decision was made to keep better performance gained from not needing synchronization.

Terminology

Entry: a configuration entry is a value accessible using a key. The keys have a dot-separated format. (e.g.: app.name)

Registering an entry: informs the framework that an entry with the given key is expected. Registering an entry allows to set a default value to be used if this entry is not provided in an app's configuration file, to enforce a certain type for this entry (for example if it needs to be an integer), and to set a list of allowed values.

Category: a category is represented by a JSON object in your configuration file, delimited by braces. Sub-categories are categories that are not at root level. For example: server.proxy is a sub-category of the server category.

Loading configuration

Automatic

By default, the configuration is loaded automatically when initializing the server with goyave.New().

Most projects need different configuration values based on the environment. For example, you won't connect to the same database if you're in local development, in a testing environment inside continuous integration pipelines, or in production. Goyave supports multiple configurations. If you are using config.Load() or the automatic loader, the framework will pick the appropriate file depending on the environment variable GOYAVE_ENV.

GOYAVE_ENVConfig file
testconfig.test.json
productionconfig.production.json
custom_envconfig.custom_env.json
local / localhost / not setconfig.json

If you want to load the config yourself using the same logic, you can do so using config.Load():

go
import (
	"fmt"
	"os"

	"goyave.dev/goyave/v5/config"
	"goyave.dev/goyave/v5/util/errors"
)

func main() {
	cfg, err := config.Load()
	if err != nil {
		fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
		os.Exit(1)
	}

	opts := goyave.Options{
		Config: cfg,
	}

	//...
}

From a file

You can load a config file using its absolute path, or path relative to your working directory.

go
import (
	"fmt"
	"os"

	"goyave.dev/goyave/v5/config"
	"goyave.dev/goyave/v5/util/errors"
)

func main() {
	cfg, err := config.LoadFrom("config.json")
	if err != nil {
		fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
		os.Exit(1)
	}

	opts := goyave.Options{
		Config: cfg,
	}

	//...
}

From JSON or embed

You also have the option of loading your configuration from a raw JSON string or an embed.

go
import (
	_ "embed"
	"fmt"
	"os"

	"goyave.dev/goyave/v5/config"
	"goyave.dev/goyave/v5/util/errors"
)

//go:embed config.json
var cfgJSON string

func main() {
	cfg, err := config.LoadJSON(cfgJSON)
	if err != nil {
		fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
		os.Exit(1)
	}

	opts := goyave.Options{
		Config: cfg,
	}

	//...
}

TIP

You can make use of build constraints to load a different config file per environment. Create one config_<env>.go file per environment.

config_local.go

go
//go:build !production && !staging

package main

import _ "embed"

//go:embed config.json
var cfgJSON string

config_production.go

go
//go:build production

package main

import _ "embed"

//go:embed config.production.json
var cfgJSON string

Then use the build tag: go run -tags production or go build -tags production.

Validation

All entries are validated. That means that the application will not start if you provided an invalid value in your config (for example if the specified port is not a number).

Each entries is registered with a default value, a type and authorized values. Raw values parsed from JSON may be converted to the expected type if the value allows it. For example, a number parsed in JSON is always float64 by default, but if the entry specifies that the type is int, the configuration loader will try to convert the number to int. If the entry is specified to be a slice, it will be converted to a slice of the expected type instead of always ending up []any.

Environment variables

You can use environment variables in your configuration file. Environment variables are identified by the following syntax: ${VARIABLE_NAME}.

json
{
    "database": {
        "host": "${DB_HOST}"
    }
}

Note: This syntax is strict. If the string doesn't start with ${ or doesn't end with }, it will not be considered an environment variable. Therefore, templating like ${VAR_1}-${VAR_2} is not supported.

string, int, float64 and bool values are supported. If the configuration entry is expected to be of one of these types, the content of the environment variable will be automatically converted as explained in the validation section above. If the conversion fails, a configuration loading error will be returned.

If an environment variable mentioned in a configuration file is not set, the configuration validation will not pass. Environment variables are not supported inside slices.

Using the configuration

All entries are accessible using dot-separated paths. For example If you want to access the name entry in the app category, the key will be app.name. You cannot retrieve an entire category at once.

go
cfg.Get("app.name") // type any
cfg.GetString("app.name")
cfg.GetBool("app.debug")
cfg.GetInt("server.port")
cfg.GetFloat("server.maxUploadSize")
cfg.GetStringSlice("path.to.slice")
cfg.GetBoolSlice("path.to.slice")
cfg.GetIntSlice("path.to.slice")
cfg.GetFloatSlice("path.to.slice")
cfg.Has("custom_entry") // Returns true if the entry exists

If the entry requested is not of the getter's expected type, the function will panic.

See the reference.

Configuration reference

App

EntryTypeDefaultNote
app.namestring"goyave"
app.environmentstring"localhost"
app.debugbooltrueWhen activated, uses human-readable log formatter instead of JSON structured logs and sends error(s) details in responses. Disable this in production!
app.defaultLanguagestring"en-US"See the Localization section

Server

EntryTypeDefaultNote
server.hoststring"127.0.0.1"
server.domainstring""Used for URL generation. Leave empty to use IP instead.
server.portint8080If set to 0, a port number is automatically chosen and can be retrieved with server.Port(). The chosen port is not reflected in the configuration.
server.writeTimeoutint10*http.Server's WriteTimeout (in seconds)
server.readTimeoutint10*http.Server's ReadTimeout (in seconds)
server.readHeaderTimeoutint10*http.Server's ReadHeaderTimeout (in seconds)
server.idleTimeoutint20*http.Server's IdleTimeout (in seconds)
server.websocketCloseTimeoutint10Maximum time for the websocket close handshake (in seconds)
server.maxUploadSizefloat6410Maximum size of the request, in MiB

Proxy

This section is used for URL generation (server.ProxyBaseURL()) if you are running your application behind a reverse proxy (such as nginx or apache). These entries don't have any impact on networking and are not required.

EntryTypeAccepted valuesDefaultNote
server.proxy.protocolstring"http", "https""http"
server.proxy.hoststringanyPublic host or domain of your proxy
server.proxy.portintany80
server.proxy.basestringany""The base path (usually starts with /)

Database

EntryTypeDefaultNote
database.connectionstring"none"See the database guide
database.hoststring"127.0.0.1"
database.portint0
database.namestring""
database.usernamestring""
database.passwordstring""
database.optionsstring""The options passed to the DSN when creating the connection.
database.maxOpenConnectionsint20
database.maxIdleConnectionsint20
database.maxLifetimeint300The maximum time (in seconds) a connection may be reused.
database.defaultReadQueryTimeoutint20000The maximum execution time for read queries (in ms)
database.defaultWriteQueryTimeoutint40000The maximum execution time for write queries (in ms)

Gorm config

EntryTypeDefault
database.config.skipDefaultTransactionboolfalse
database.config.dryRunboolfalse
database.config.prepareStmtbooltrue
database.config.disableNestedTransactionboolfalse
database.config.allowGlobalUpdateboolfalse
database.config.disableAutomaticPingboolfalse
database.config.disableForeignKeyConstraintWhenMigratingboolfalse

TIP

See Gorm's documentation for more details.

Custom configuration entries

Configuration can be expanded. It is very likely that a plugin or a package you're developing is using some form of options. These options can be added to the configuration system so it is not needed to set them in the code or to make some wiring.

You can register a new config entry and its validation using config.Register().

Each module should register its config entries in an init() function, even if they don't have a default value, in order to ensure they will be validated. Each module should use its own category and use a name both expressive and unique to avoid collisions. For example, the auth package registers, among others, auth.basic.username and auth.jwt.expiry, thus creating a category for its package, and two subcategories for its features.

To register an entry without a default value (only specify how it will be validated), set Entry.Value to nil. You can set Entry.Required to true to prevent nil values.

Panics if an entry already exists for this key and is not identical to the one passed as parameter of this function. On the other hand, if the entries are identical, no conflict is expected so the configuration is left in its current state.

go
import (
	"reflect"

	"goyave.dev/goyave/v5/config"
)

func init() {
	config.Register("customCategory.customEntry", config.Entry{
		Value:            "default value",
		Type:             reflect.String,
		IsSlice:          false,
		AuthorizedValues: []any{},
		Required:         true,
	})
}