Goal

Fetch real job listings from JSONFakery (GET /jobs), normalize them into HireBoard job cards with transformResponse, and render them on the protected /jobs route.

1. Create src/apis/jobsApi.js

Add getJobs against the live JSONFakery API:

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { getCookie } from "../utils/cookies";

function mapJsonFakeryJob(job) {
  return {
    id: job.id,
    title: job.title,
    company: job.company,
    location: job.location,
    type: job.employment_type || "Full-time",
    salary:
      job.salary_from && job.salary_to
        ? `$${job.salary_from.toLocaleString()} - $${job.salary_to.toLocaleString()}`
        : "$--",
    tags: [job.job_category, job.is_remote_work ? "Remote" : "Onsite"].filter(
      Boolean,
    ),
    description: job.description,
  };
}

export const jobsApi = createApi({
  reducerPath: "jobsApi",
  baseQuery: fetchBaseQuery({
    baseUrl: "https://jsonfakery.com",
    prepareHeaders: (headers, { getState }) => {
      const token = getState().auth.token || getCookie("hireboard-token");
      if (token) {
        headers.set("Authorization", `Bearer ${token}`);
      }
      return headers;
    },
  }),
  tagTypes: ["Jobs"],
  endpoints: (builder) => ({
    login: builder.mutation({
      query: ({ username, password }) => ({
        url: "/auth/login",
        method: "POST",
        body: { username, password, expiresInMins: 60 },
      }),
    }),
    getJobs: builder.query({
      query: () => "/jobs",
      transformResponse: (response) => (response || []).map(mapJsonFakeryJob),
      providesTags: ["Jobs"],
    }),
  }),
});

export const { useGetJobsQuery } = jobsApi;

Chunks explained

Chunk What it does
GET /jobs Reads live job data from JSONFakery
transformResponse Normalizes API job fields for the UI
mapJsonFakeryJob Converts salary_from, salary_to, job_category, and more
providesTags: ["Jobs"] Cache label for later invalidation
Bearer header Reuses the cookie/token from login

JSONFakery returns a real jobs array, so we no longer need dummy product mapping. The important part is still a live network request + RTK Query caching.

2. JobCard.jsx

function JobCard({ job }) {
  return (
    <article className="flex flex-col rounded-xl border border-slate-200 bg-card p-5 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-700 dark:bg-slate-900">
      <div className="mb-3 flex items-start justify-between gap-3">
        <div>
          <h3 className="text-lg font-semibold text-ink dark:text-white">
            {job.title}
          </h3>
          <p className="text-sm text-brand-600">{job.company}</p>
        </div>
        <span className="rounded-full bg-brand-50 px-2.5 py-1 text-xs font-medium capitalize text-brand-700 dark:bg-brand-900/40 dark:text-brand-100">
          {job.type}
        </span>
      </div>

      <p className="mb-4 flex-1 text-sm text-muted dark:text-slate-400">
        {job.description}
      </p>

      <div className="mb-4 flex flex-wrap gap-2">
        {job.tags.map((tag) => (
          <span
            key={tag}
            className="rounded-md bg-slate-100 px-2 py-1 text-xs capitalize text-slate-600 dark:bg-slate-800 dark:text-slate-300"
          >
            {tag}
          </span>
        ))}
      </div>

      <div className="flex items-center justify-between border-t border-slate-100 pt-3 text-sm dark:border-slate-800">
        <span className="text-muted dark:text-slate-400">{job.location}</span>
        <span className="font-medium text-ink dark:text-white">
          {job.salary}
        </span>
      </div>
    </article>
  );
}

export default JobCard;

3. JobsList.jsx

import { useGetJobsQuery } from "../apis/jobsApi";
import JobCard from "./JobCard";

function JobsList() {
  const { data: jobs, isLoading, isError, error, refetch } = useGetJobsQuery();

  if (isLoading) {
    return (
      <p className="text-muted dark:text-slate-400" role="status">
        Loading open roles from JSONFakery…
      </p>
    );
  }

  if (isError) {
    return (
      <div className="rounded-md bg-red-50 p-4 text-sm text-red-700 dark:bg-red-950 dark:text-red-300">
        <p>
          Could not load jobs: {error?.error || error?.data || "Unknown error"}
        </p>
        <button
          type="button"
          onClick={refetch}
          className="mt-2 font-medium underline"
        >
          Try again
        </button>
      </div>
    );
  }

  return (
    <section>
      <div className="mb-6 flex items-end justify-between gap-4">
        <div>
          <h2 className="text-2xl font-semibold">Open roles</h2>
          <p className="text-sm text-muted dark:text-slate-400">
            {jobs.length} positions · GET /jobs (live API)
          </p>
        </div>
        <button
          type="button"
          onClick={refetch}
          className="rounded-md border border-slate-300 px-3 py-2 text-sm font-medium hover:bg-slate-100 dark:border-slate-600 dark:hover:bg-slate-800"
        >
          Refresh
        </button>
      </div>

      <div className="grid gap-4 sm:grid-cols-2">
        {jobs.map((job) => (
          <JobCard key={job.id} job={job} />
        ))}
      </div>
    </section>
  );
}

export default JobsList;

Hook chunks explained

Chunk What it does
useGetJobsQuery() Fires (or reads cache for) the live request
isLoading / isError Request lifecycle UI
refetch Hit the network again

4. Use it on the jobs route

Replace the placeholder in src/routes/_authenticated/jobs.jsx:

import { createFileRoute } from "@tanstack/react-router";
import JobsList from "../../components/JobsList";

export const Route = createFileRoute("/_authenticated/jobs")({
  component: JobsPage,
});

function JobsPage() {
  return <JobsList />;
}

5. Confirm in DevTools

  1. Network → jobs → status 200.
  2. Redux → jobsApi.queries shows cached data.
  3. Visiting /jobs while logged out still redirects to /login.

Check

  • Signed-in /jobs shows a grid from the live API.
  • Refresh button triggers another network call (or serves cache, then refetch).

Assessment: Taskly : RTK Query todos

Fetch real todos with an RTK Query query:

GET https://dummyjson.com/todos?limit=20

Map each item into a Taskly card (id, todo text, completed, userId). Show loading and error UI on /tasks. Attach Bearer token via prepareHeaders.

Done when: signed-in /tasks shows a live DummyJSON todo list.

Next: save applications into a client slice and show them on /saved.