Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guide: How to Integrate ZIO‌ HTTP with ZIO‌ Config #2810

Merged
merged 9 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,9 @@ lazy val zioHttpExample = (project in file("zio-http-example"))
.settings(libraryDependencies ++= Seq(`jwt-core`, `zio-schema-json`))
.settings(
libraryDependencies ++= Seq(
"dev.zio" %% "zio-config" % "4.0.2",
"dev.zio" %% "zio-config-typesafe" % "4.0.2",
"dev.zio" %% "zio-config" % ZioConfigVersion,
"dev.zio" %% "zio-config-typesafe" % ZioConfigVersion,
"dev.zio" %% "zio-config-magnolia" % ZioConfigVersion,
"dev.zio" %% "zio-metrics-connectors" % "2.3.1",
"dev.zio" %% "zio-metrics-connectors-prometheus" % "2.3.1",
),
Expand Down Expand Up @@ -323,13 +324,16 @@ lazy val docs = project
libraryDependencies ++= Seq(
`jwt-core`,
"dev.zio" %% "zio-test" % ZioVersion,
"dev.zio" %% "zio-config" % "4.0.2",
"dev.zio" %% "zio-config" % ZioConfigVersion,
"dev.zio" %% "zio-config-magnolia" % ZioConfigVersion,
"dev.zio" %% "zio-config-typesafe" % ZioConfigVersion
),
publish / skip := true,
mdocVariables ++= Map(
"ZIO_VERSION" -> ZioVersion,
"ZIO_SCHEMA_VERSION" -> ZioSchemaVersion,
"ZIO_VERSION" -> ZioVersion,
),
"ZIO_CONFIG_VERSION" -> ZioConfigVersion,
)
)
.dependsOn(zioHttpJVM)
.enablePlugins(WebsitePlugin)
Expand Down
2 changes: 1 addition & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ ZIO HTTP provides built-in support for JSON serialization and deserialization us

### How Can I Handle CORS Requests in ZIO HTTP?

ZIO has several middlewares including `CORS` that can be used to handle cross-origin resource sharing requests. Check out the [Middleware](./reference/aop/middleware) section in the documentation for more details.
ZIO has several middlewares including `CORS` that can be used to handle cross-origin resource sharing requests. Check out the [Middleware](./reference/aop/middleware.md) section in the documentation for more details.

### How Does ZIO HTTP Handle Errors?

Expand Down
305 changes: 305 additions & 0 deletions docs/guides/integrate-with-zio-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
---
id: integration-with-zio-config
title: "How to Integrate with ZIO Config"
sidebar_label: Integration with ZIO Config
---

When building HTTP applications, it is common to have configuration settings that need to be loaded from various sources such as environment variables, system properties, or configuration files. It is essential especially when deploying applications to different environments like development, testing, and production, or we want to have a cloud-native application that can be configured dynamically.

ZIO HTTP provides seamless integration with [ZIO Config](https://zio.dev/zio-config/), a powerful configuration library for ZIO, to manage configurations in your HTTP applications.

In this guide, we will learn how to integrate ZIO HTTP with ZIO Config to load configuration settings for our HTTP applications.

## ZIO Config Overview

The ZIO core library has a built-in configuration system that allows us to define a type-safe configuration schema, load configurations from various sources, validate configurations, and access configuration settings in a functional way.

We can define a configuration schema for any custom data type. For example, if we have a `DatabaseConfig` case class as follows:

```scala mdoc:silent
case class DatabaseConfig(
url: String,
username: String,
password: String,
poolSize: Int,
)
```

We can derive a configuration schema for `DatabaseConfig` using ZIO Config as follows:

```scala mdoc:silent
import zio._
import zio.config._
import zio.config.magnolia._

object DatabaseConfig {
val config: Config[DatabaseConfig] =
DeriveConfig.deriveConfig[DatabaseConfig]
.mapKey(toSnakeCase)
.nested("database")
}
```

Now, we can load the configuration settings for `DatabaseConfig` by calling `ZIO.config(DatabaseConfig.config)`:

```scala mdoc:compile-only
import zio._

object MainApp extends ZIOAppDefault {
def run = {
for {
config <- ZIO.config(DatabaseConfig.config)
_ <- ZIO.debug("Just started right now!")
_ <- ZIO.debug(s"Connecting to the database: ${config.url}")
} yield ()
}
}
```

By default, ZIO will load the configs from environment variables, so we need to set the following environment variables:

```bash
export DATABASE_URL="jdbc:postgresql://localhost:5432/mydb"
export DATABASE_USERNAME="admin"
export DATABASE_PASSWORD="password"
export DATABASE_POOL_SIZE=10
```

## Loading Configuration Settings from a File

As we mentioned earlier, by default, ZIO loads configurations from environment variables. However, we can change the `ConfigProvider` to load configurations from other sources such as system properties, console, and system properties. All of these are built-in providers in the ZIO core library.

ZIO Config also provides more advanced `ConfigProvider`s such as HOCON, JSON, YAML, and XML. Based on the configuration format, we need to add one of the following dependencies to our project:

```scala
libraryDependencies += "dev.zio" %% "zio-config-typesafe" % "@ZIO_CONFIG_VERSION@" // HOCON
libraryDependencies += "dev.zio" %% "zio-config-yaml" % "@ZIO_CONFIG_VERSION@" // YAML and JSON
libraryDependencies += "dev.zio" %% "zio-config-xml" % "@ZIO_CONFIG_VERSION@" // XML
```

Assuming we have an `application.conf` file inside the `resources` directory with the following content:

```hocon
database {
url: "jdbc:mysql://localhost:3306/mydatabase"
url: ${?DATABASE_URL}
username: "user"
username: ${?DATABASE_USERNAME}
password: "password"
password: ${?DATABASE_PASSWORD}
pool_size: 20
pool_size: ${?DATABASE_POOL_SIZE}
}
```

Then, we can load it using the `ConfigProvider.fromResourcePath` method:

```scala mdoc:compile-only
import zio._
import zio.http._
import zio.config.typesafe._

object MainApp extends ZIOAppDefault {
override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
Runtime.setConfigProvider(ConfigProvider.fromResourcePath())

def run =
for {
config <- ZIO.config(DatabaseConfig.config)
_ <- ZIO.debug("Just started right now!")
_ <- ZIO.debug(s"Connecting to the database: ${config.url}")
} yield ()
}
```

## Client and Server Configuration

Both `Client` and `Server` have the `default` layer that requires no configuration and provides an instance of `Client` and `Server` with default settings:

```scala mdoc:invisible
import zio._
import zio.http._
```

```scala mdoc:compile-only
object Client {
val default: ZLayer[Any, Throwable, Client] = ???
}

object Server {
val default: ZLayer[Any, Throwable, Server] = ???
}
```

In some cases, we need to customize the client or server settings such as timeouts, host, port, and other parameters. To do that, ZIO HTTP provides `live` and `customized` layers that require additional configuration settings:

```scala mdoc:invisible
import zio._
import zio.http._
import zio.http.netty._
```

```scala mdoc:compile-only
object Client {
case class Config(
// configuration settings for client
)

val live : ZLayer[Client.Config with NettyConfig with DnsResolver, Throwable, Client] = ???
def customized: ZLayer[Client.Config with ClientDriver with DnsResolver, Throwable, Client] = ???
}

object Server {
case class Config(
// configuration settings for server
)

val live : ZLayer[Server.Config, Throwable, Server] = ???
val customized: ZLayer[Server.Config & NettyConfig, Throwable, Server] = ???
}
```

So, to have a customized client or server, we need to provide configuration layers to satisfy the required dependencies. For example, to create a `live` server, we need to provide a `ZLayer` that produces a `Server.Config`.

For a practical example, see the following code which enables the response compression in the server:

```scala mdoc:passthrough
import utils._

printSource("zio-http-example/src/main/scala/example/ServerResponseCompression.scala")
```

In the above example, we updated the default server configuration to enable the response compression. Finally, we provided the `Server.live` and our customized config layer to the `Server.serve` method.

### Predefined Configuration Schemas

Until now, we changed the server configuration programmatically inside the code. But what if we want to load the client or server configuration from a file, e.g. `application.conf`? We need to have a configuration schema for the client and server settings, i.e. `zio.Config[Client.Config]` and `zio.Config[Server.Config]`. Fortunately, ZIO HTTP provides these configuration schemas by default.

Before going further, let's take a look at the `Server.Config` and `Client.Config` and see how are they defined in ZIO HTTP:

```scala mdoc:compile-only
object Client {
case class Config(
// configuration settings for client
)
object Config {
// Configuration Schema for Cleint.Config
val config: zio.Config[Client.Config] = ???

// default configuration for Client.Config
lazy val default: Client.Config = ???
}
}

object Server {
case class Config(
// configuration settings for server
)
object Config {
// configuration schema for Server.Config
val config: zio.Config[Server.Config] = ???

// default configuration for Server.Config
lazy val default: Server.Config = ???
}
}
```

The `Server` and `Client` modules have predefined config schema, i.e. `Server.Config.config` and `Client.Config.config`, that can be used to load the server/client configuration from the environment, system properties, or any other configuration sources.

## Loading Configuration Settings from Environment Variables

As the ZIO HTTP provided these configuration schemas by default, we can easily use them to load the configuration settings from the considered sources using the corresponding `ConfigProvider`:

```scala mdoc:compile-only
import zio._
import zio.http._

object MainApp extends ZIOAppDefault {
def run = {
Server
.install(
Routes(
Method.GET / "hello" -> handler(Response.text("Hello, world!")),
),
)
.flatMap(port => ZIO.debug(s"Sever started on http://localhost:$port") *> ZIO.never)
.provide(
Server.live,
ZLayer.fromZIO(
ZIO.config(Server.Config.config.mapKey(_.replace('-', '_'))),
),
)
}
}
```

```shell
export BINDING_HOST=localhost
export BINDING_PORT=8081
```

:::note
In the above example, we used the `mapKey` method to replace the `-` character with `_` in the configuration keys. This is because the environment variables do not allow the `-` character in the key names.
:::

### Loading Configuration Settings from an HOCON File

By changing the `ConfigProvider` to `ConfigProvider.fromResourcePath()`, we can load the server configuration from the `application.conf` file:

```hocon
zio.http.server {
binding_port: 8083
binding_host: localhot
}
```

```scala mdoc:invisible:reset
```

```scala mdoc:passthrough
import utils._
printSource("zio-http-example/src/main/scala/example/config/LoadServerConfigFromHoconFile.scala")
```

Instead of providing two layers (`Server.live` and `ZLayer.fromZIO(ZIO.config(Server.Config.config))`) to the `Server.serve` method, we can combine them into a single layer using the `Server.configured` layer:

```scala mdoc:passthrough
import utils._
printSource("zio-http-example/src/main/scala/example/config/HoconWithConfiguredLayerExample.scala")
```

### Customized Layers

If we need to have more control, the `Server` and `Client` companion objects have also `customized` layers that require additional configuration settings to customize the underlying settings for the server and client:

- `Server.customized` is a layer that requires a `Server.Config` and `NettyConfig` and returns a `Server` layer.
- `Client.customized` is a layer that requires a `Client.Config`, `NettyConfig`, and `DnsResolver` and returns a `Client` layer.

```scala mdoc:silent
import zio._
import zio.http._
import zio.http.netty._
```

```scala mdoc:compile-only
object Clinet {
case class Config(
// configuration settings for client
)

val customized: ZLayer[Config with ClientDriver with DnsResolver, Throwable, Client] = ???
}

object Server {
case class Config(
// configuration settings for server
)

val customized: ZLayer[Config & NettyConfig, Throwable, Server] = ???
}
```

## Summary

In this guide, we learned how to integrate ZIO HTTP with ZIO Config to load configuration settings for our HTTP applications. We also learned how to load configuration settings from environment variables, system properties, and configuration files, such as HOCON and YAML using ZIO Config's configuration providers.
Loading
Loading