diff --git a/frontend/src/metabase-lib/fields.ts b/frontend/src/metabase-lib/fields.ts
new file mode 100644
index 0000000000000000000000000000000000000000..305c013d2114de48668a3359da4b87650fde7bff
--- /dev/null
+++ b/frontend/src/metabase-lib/fields.ts
@@ -0,0 +1,19 @@
+import * as ML from "cljs/metabase.lib.js";
+import type { Clause, ColumnMetadata, Query } from "./types";
+
+const DEFAULT_STAGE_INDEX = -1;
+
+export function fields(
+  query: Query,
+  stageIndex = DEFAULT_STAGE_INDEX,
+): Clause[] {
+  return ML.fields(query, stageIndex);
+}
+
+export function withFields(
+  query: Query,
+  stageIndex: number,
+  newFields: ColumnMetadata[],
+): Query {
+  return ML.with_fields(query, stageIndex, newFields);
+}
diff --git a/frontend/src/metabase-lib/v2.ts b/frontend/src/metabase-lib/v2.ts
index df421ff6a4067a1cca1d5bcf5b7c7bed66934378..f20a6b4659a02e37a443af8d9ab53fe973e17823 100644
--- a/frontend/src/metabase-lib/v2.ts
+++ b/frontend/src/metabase-lib/v2.ts
@@ -4,6 +4,7 @@ export * from "./column_types";
 export * from "./comparison";
 export * from "./metadata";
 export * from "./breakout";
+export * from "./fields";
 export * from "./limit";
 export * from "./order_by";
 export * from "./query";
diff --git a/src/metabase/lib/field.cljc b/src/metabase/lib/field.cljc
index 3a49797e7081ece877922cbb99d9ea6ed81c380c..a5ae7b41903513ac889e39e391c4bef67b9c4495 100644
--- a/src/metabase/lib/field.cljc
+++ b/src/metabase/lib/field.cljc
@@ -442,7 +442,7 @@
     (:name field-metadata)))
 
 (defn with-fields
-  "Specify the `:fields` for a query."
+  "Specify the `:fields` for a query. Pass `nil` or an empty sequence to remove `:fields`."
   ([xs]
    (fn [query stage-number]
      (with-fields query stage-number xs)))
@@ -456,10 +456,11 @@
                                    (x query stage-number)
                                    x)))
                   xs)]
-     (lib.util/update-query-stage query stage-number assoc :fields xs))))
+     (lib.util/update-query-stage query stage-number u/assoc-dissoc :fields (not-empty xs)))))
 
 (defn fields
-  "Fetches the `:fields` for a query."
+  "Fetches the `:fields` for a query. Returns `nil` if there are no `:fields`. `:fields` should never be empty; this is
+  enforced by the Malli schema."
   ([query]
    (fields query -1))
   ([query stage-number]
diff --git a/src/metabase/lib/js.cljs b/src/metabase/lib/js.cljs
index 4494625a89e2c02ef7beb43ae9a5a7b891e32723..a5759eade3e5ee18a004e25790ad51bf3522a0b5 100644
--- a/src/metabase/lib/js.cljs
+++ b/src/metabase/lib/js.cljs
@@ -338,3 +338,18 @@
   [[available-binning-strategies]] to get `available-aggregation`."
   [aggregation-operator]
   (to-array (lib.core/aggregation-operator-columns aggregation-operator)))
+
+(defn ^:export fields
+  "Get the current `:fields` in a query. Unlike the lib core version, this will return an empty sequence if `:fields` is
+  not specified rather than `nil` for JS-friendliness."
+  ([a-query]
+   (fields a-query -1))
+  ([a-query stage-number]
+   (to-array (lib.core/fields a-query stage-number))))
+
+(defn ^:export with-fields
+  "Specify the `:fields` for a query. Pass an empty sequence or `nil` to remove `:fields`."
+  ([a-query new-fields]
+   (with-fields a-query -1 new-fields))
+  ([a-query stage-number new-fields]
+   (lib.core/with-fields a-query stage-number new-fields)))
diff --git a/test/metabase/lib/field_test.cljc b/test/metabase/lib/field_test.cljc
index ed9bd2ffbd8e548ee9abc1bfb4fdb515364ce938..f6f0df4acf62d5a3f7a53307d860cd63521ef97e 100644
--- a/test/metabase/lib/field_test.cljc
+++ b/test/metabase/lib/field_test.cljc
@@ -458,3 +458,29 @@
                 :lib/source-column-alias  "avg_count"
                 :lib/desired-column-alias "avg_count"}]
               (lib.metadata.calculation/metadata query))))))
+
+(deftest ^:parallel with-fields-test
+  (let [query           (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
+                            (lib/with-fields [(lib/field "VENUES" "ID") (lib/field "VENUES" "NAME")]))
+        fields-metadata (fn [query]
+                          (map (partial lib.metadata.calculation/metadata query)
+                               (lib/fields query)))
+        metadatas       (fields-metadata query)]
+    (is (=? [{:name "ID"}
+             {:name "NAME"}]
+            metadatas))
+    (testing "Set fields with metadatas"
+      (let [fields' [(last metadatas)]
+            query'  (lib/with-fields query fields')]
+        (is (=? [{:name "NAME"}]
+                (fields-metadata query')))))
+    (testing "remove fields by passing"
+      (doseq [new-fields [nil []]]
+        (testing (pr-str new-fields)
+          (let [query' (lib/with-fields query new-fields)]
+            (is (empty? (fields-metadata query')))
+            (letfn [(has-fields? [query]
+                      (get-in query [:stages 0 :fields]))]
+              (is (has-fields? query)
+                  "sanity check")
+              (is (not (has-fields? query'))))))))))