diff --git a/.changeset/thin-coats-bake.md b/.changeset/thin-coats-bake.md
new file mode 100644
index 00000000000..4e24c68f6b7
--- /dev/null
+++ b/.changeset/thin-coats-bake.md
@@ -0,0 +1,8 @@
+---
+"@effect/platform": patch
+---
+
+`Url` module has been introduced:
+
+- immutable setters with dual-function api
+- integration with `UrlParams`
diff --git a/packages/platform/src/Error.ts b/packages/platform/src/Error.ts
index d1354823527..d042b7cf08e 100644
--- a/packages/platform/src/Error.ts
+++ b/packages/platform/src/Error.ts
@@ -65,7 +65,7 @@ export declare namespace PlatformError {
   export interface Base {
     readonly [PlatformErrorTypeId]: typeof PlatformErrorTypeId
     readonly _tag: string
-    readonly module: "Clipboard" | "Command" | "FileSystem" | "KeyValueStore" | "Path" | "Stream" | "Terminal"
+    readonly module: "Clipboard" | "Command" | "FileSystem" | "KeyValueStore" | "Path" | "Stream" | "Terminal" | "Url"
     readonly method: string
     readonly message: string
   }
diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts
new file mode 100644
index 00000000000..6a43f606c34
--- /dev/null
+++ b/packages/platform/src/Url.ts
@@ -0,0 +1,150 @@
+/**
+ * @since 1.0.0
+ */
+import * as Either from "effect/Either"
+import { dual } from "effect/Function"
+import * as Error from "./Error.js"
+import * as UrlParams from "./UrlParams.js"
+
+/** @internal */
+const immutableSetter = <M>(clone: (mutable: M) => M) =>
+<P extends keyof M>(property: P): {
+  (value: M[P]): (mutable: M) => M
+  (mutable: M, value: M[P]): M
+} =>
+  dual(2, (mutable: M, value: M[P]) => {
+    const result = clone(mutable)
+    result[property] = value
+    return result
+  })
+/** @internal */
+const immutableURLSetter = immutableSetter<URL>((url) => new URL(url))
+/**
+ * @since 1.0.0
+ * @category constructors
+ */
+export const make: {
+  (url: string | URL, base?: string | URL | undefined): Either.Either<URL, Error.BadArgument>
+} = (url, base) =>
+  Either.try({
+    try: () => new URL(url, base),
+    catch: (cause) =>
+      Error.BadArgument({
+        module: "Url",
+        method: "make",
+        message: cause instanceof globalThis.Error ? cause.message : "Invalid input"
+      })
+  })
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setHash: {
+  (hash: string): (url: URL) => URL
+  (url: URL, hash: string): URL
+} = immutableURLSetter("hash")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setHost: {
+  (host: string): (url: URL) => URL
+  (url: URL, host: string): URL
+} = immutableURLSetter("host")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setHostname: {
+  (hostname: string): (url: URL) => URL
+  (url: URL, hostname: string): URL
+} = immutableURLSetter("hostname")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setHref: {
+  (href: string): (url: URL) => URL
+  (url: URL, href: string): URL
+} = immutableURLSetter("href")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setPassword: {
+  (password: string): (url: URL) => URL
+  (url: URL, password: string): URL
+} = immutableURLSetter("password")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setPathname: {
+  (pathname: string): (url: URL) => URL
+  (url: URL, pathname: string): URL
+} = immutableURLSetter("pathname")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setPort: {
+  (port: string): (url: URL) => URL
+  (url: URL, port: string): URL
+} = immutableURLSetter("port")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setProtocol: {
+  (protocol: string): (url: URL) => URL
+  (url: URL, protocol: string): URL
+} = immutableURLSetter("protocol")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setSearch: {
+  (search: string): (url: URL) => URL
+  (url: URL, search: string): URL
+} = immutableURLSetter("search")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setUsername: {
+  (username: string): (url: URL) => URL
+  (url: URL, username: string): URL
+} = immutableURLSetter("username")
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const setUrlParams: {
+  (searchParams: UrlParams.UrlParams): (url: URL) => URL
+  (url: URL, username: UrlParams.UrlParams): URL
+} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) => {
+  const result = new URL(url)
+  result.search = UrlParams.toString(searchParams)
+  return result
+})
+/**
+ * @since 1.0.0
+ * @category getters
+ */
+export const urlParams: {
+  (url: URL): UrlParams.UrlParams
+} = (url) => UrlParams.fromInput(url.searchParams)
+/**
+ * @since 1.0.0
+ * @category utils
+ */
+export const modifyUrlParams: {
+  (f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): (url: URL) => URL
+  (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): URL
+} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) => {
+  const urlParams = UrlParams.fromInput(url.searchParams)
+  const newUrlParams = f(urlParams)
+  const result = new URL(url)
+  result.search = UrlParams.toString(newUrlParams)
+  return result
+})
diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts
index bf627d75004..e0a8523001e 100644
--- a/packages/platform/src/index.ts
+++ b/packages/platform/src/index.ts
@@ -249,6 +249,11 @@ export * as Terminal from "./Terminal.js"
  */
 export * as Transferable from "./Transferable.js"
 
+/**
+ * @since 1.0.0
+ */
+export * as Url from "./Url.js"
+
 /**
  * @since 1.0.0
  */
diff --git a/packages/platform/test/Url.test.ts b/packages/platform/test/Url.test.ts
new file mode 100644
index 00000000000..c9abb7426f2
--- /dev/null
+++ b/packages/platform/test/Url.test.ts
@@ -0,0 +1,28 @@
+import * as Url from "@effect/platform/Url"
+import * as UrlParams from "@effect/platform/UrlParams"
+import { assert, describe, it } from "@effect/vitest"
+import { Effect } from "effect"
+
+describe("Url", () => {
+  const testURL = new URL("https://example.com/test")
+
+  describe("make", () => {
+    it.effect("immutable", () =>
+      Effect.gen(function*() {
+        const url = yield* Url.make(testURL)
+        assert.notStrictEqual(url, testURL)
+      }))
+  })
+  describe("setters", () => {
+    it("immutable", () => {
+      const hashUrl = Url.setHash(testURL, "test")
+      assert.notStrictEqual(hashUrl, testURL)
+      assert.strictEqual(hashUrl.toString(), "https://example.com/test#test")
+    })
+  })
+  it("modifyUrlParams", () => {
+    const paramsUrl = Url.modifyUrlParams(testURL, (x) => UrlParams.append(x, "key", "value"))
+    assert.notStrictEqual(paramsUrl, testURL)
+    assert.strictEqual(paramsUrl.toString(), "https://example.com/test?key=value")
+  })
+})