Separating effects from business logic

With your example function, I guess it would look like this:

(def rss-route
  ["/dev/subscriptions/add/rss"
   {:name :app.subscriptions.add/rss
    :post (fn [{:keys [session params]
                {:keys [uid]} :session
                {:keys [url]} :params
                :as ctx}]
            (handle-add-rss-sub ctx url uid))}])

(defn extract-feed-urls-from
  [http-response url]
  (->> (lib.rss/parse-urls (assoc http-response :url url))
       (mapv :url)
       (take 20)
       vec))

(defn retrieve-feed-urls
  [url]
  {:ok :feed-urls-retrieved
   :url url
   :options {"User-Agent" "https://yakread.com"}
   :query #(extract-feed-urls-from % url)})

(defn commit-feed-urls-retrieved
  [{:keys [url options query]}]
  (-> url (http/get options) query))

(defn fix-and-retrieve-feed-urls
  [unfixed-url]
  (-> unfixed-url
      (lib.rss/fix-url)
      (retrieve-feed-urls)))

(defn add-rss-sub
  [feed-urls uid]
  (if (empty? feed-urls)
    {:error :invalid-rss-feed
     :msg "invalid-rss-feed"}
    {:ok :rss-sub-added
     :query (for [url feed-urls]
              {:db/doc-type :conn/rss
               :db.op/upsert {:conn/user uid
                              :conn.rss/url url}
               :conn.rss/subscribed-at :db/now})
     :added-count (count feed-urls)}))

(defn commit-rss-sub-added
  [ctx {:keys [query]}]
  (biff/submit-tx ctx query))

(defn handle-add-rss-sub
  [ctx url uid]
  (let [feed-urls-retrieved (fix-and-retrieve-feed-urls url)
        feed-urls (commit-feed-urls-retrieved feed-urls-retrieved)
        rss-sub-added (add-rss-sub feed-urls uid)]
    (if (:ok rss-sub-added)
      (do (commit-rss-sub-added ctx rss-sub-added)
          {:status 303
           :biff.response/route-name :app.subscriptions/page
           :biff.response/route-params {:added-feeds (:added-count rss-sub-added)}})
      {:status 303
       :biff.response/route-name :app.subscriptions.add/page
       :biff.response/route-params {:error (:msg rss-sub-added)}})))