Purpose: Understanding how Pathom resolvers work - the "why" behind the patterns.
Pathom is a graph query planner. You declare what data resolvers can provide, and Pathom figures out how to satisfy queries.
┌─────────────────────────────────────────────────────────────┐
│ EQL Query │
│ [{:swapi/all-people [:person/name :person/homeworld]}] │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Query Planner │
│ "I need all-people, then person details, then homeworld" │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Resolvers │
│ Each declares: "Give me X, I'll provide Y" │
└─────────────────────────────────────────────────────────────┘
Key insight: You don't write fetch logic. You declare capabilities. Pathom builds the plan.
Every resolver declares what it needs (input) and what it provides (output):
(pco/defresolver person-resolver [{:person/keys [id]}]
{::pco/input [:person/id] ; "Give me a person ID..."
::pco/output [:person/name ; "...I'll give you name, height, mass"
:person/height
:person/mass]}
(fetch-person id))Pathom chains resolvers by matching outputs to inputs:
Query: [:person/name] for person "1"
Plan:
1. I have [:person/id "1"]
2. person-resolver needs :person/id, provides :person/name ✓
3. Run person-resolver → done
No input required - these start query chains:
(pco/defresolver all-people [env params]
{::pco/output [{:swapi/all-people [:person/id]}]}
{:swapi/all-people (fetch-all-people)})Require an identity, provide details:
(pco/defresolver person-details [{:person/keys [id]}]
{::pco/input [:person/id]
::pco/output [:person/name :person/height]}
(fetch-person id))Transform existing data:
(pco/defresolver weather-from-city [{:keys [ip-info/city]}]
{::pco/input [:ip-info/city]
::pco/output [:weather/temp-c]}
(fetch-weather-for city))Given these resolvers:
all-people→ provides[:person/id]person-details→ needs:person/id, provides[:person/name :person/homeworld-id]planet-details→ needs:planet/id, provides[:planet/name]
Query: [{:swapi/all-people [:person/name {:person/homeworld [:planet/name]}]}]
Pathom's plan:
- Run
all-people→ get list with:person/id - For each person, run
person-details→ get:person/name,:person/homeworld-id - For each homeworld, run
planet-details→ get:planet/name - Assemble and return
You didn't write this orchestration - Pathom derived it from input/output declarations.
Resolvers receive env - context for the request:
(pco/defresolver search-resolver [{:keys [query-params]} params]
{::pco/output [{:results [:entity/id]}]}
(let [term (:search-term query-params)] ; Access request params
{:results (search term)}))Common env keys:
:query-params- Parameters passed with the query- Custom keys injected by your parser setup
Pathom provides error boundaries - one resolver failing doesn't crash the query.
Critical rule: Always return a map, never throw:
;; ❌ Throws on error - breaks entire query
(pco/defresolver bad-resolver [{:person/keys [id]}]
{::pco/output [:person/name]}
(fetch-person id)) ; might throw!
;; ✅ Graceful - returns empty map on failure
(pco/defresolver good-resolver [{:person/keys [id]}]
{::pco/output [:person/name]}
(try
(or (fetch-person id) {})
(catch Exception e
(log/error e "Failed" {:id id})
{})))Resolvers must return maps. nil breaks Pathom.
;; ❌ Returns nil if not found
(fetch-person id)
;; ✅ Always a map
(or (fetch-person id) {})Query asks for :person/name, resolver provides :people/name - won't match.
Resolver needs :person/id but query doesn't provide a way to get it.
Resolver defined but not added to parser's resolver list.
(require '[us.whitford.fulcro-radar.api :as radar])
(def p (radar/get-parser))
;; All resolvers by category
(->> (p {} [:radar/pathom-env]) :radar/pathom-env :resolvers)
;; Root resolvers (entry points)
(->> ... :resolvers :root (map (juxt :name :output)))
;; Entity resolvers (by-id)
(->> ... :resolvers :entity (map (juxt :name :input :output)))