Fetching and Displaying the Post Feed
The homepage of Bulletin is a feed of posts — titles, authors, vote counts, and comment counts. This lesson wires up the GET /posts endpoint and builds the component that renders the results.
The posts API call
Section titled “The posts API call”Add a getPosts function to your API client:
import apiClient from './apiClient'
export interface Post { id: number title: string body: string userId: number username: string upvotes: number commentCount: number createdAt: string}
export async function getPosts(): Promise<Post[]> { const res = await apiClient.get<Post[]>('/posts') return res.data}The shape mirrors what the Express endpoint returns. Typing it here means TypeScript catches mismatches throughout the component tree.
The PostFeed component
Section titled “The PostFeed component”import { useEffect, useState } from 'react'import { Link } from 'react-router-dom'import { getPosts, Post } from '../api/postsApi'
export default function PostFeed() { const [posts, setPosts] = useState<Post[]>([]) const [loading, setLoading] = useState(true) const [error, setError] = useState<string | null>(null)
useEffect(() => { getPosts() .then(setPosts) .catch(() => setError('Failed to load posts.')) .finally(() => setLoading(false)) }, [])
if (loading) return <p>Loading...</p> if (error) return <p>{error}</p> if (posts.length === 0) return <p>No posts yet. Be the first!</p>
return ( <ul> {posts.map(post => ( <li key={post.id}> <Link to={`/posts/${post.id}`}> <strong>{post.title}</strong> </Link> <span> by {post.username}</span> <span> · {post.upvotes} votes · {post.commentCount} comments</span> </li> ))} </ul> )}The three-state pattern — loading, error, data — is the same one from Module 08. Loading shows a spinner or placeholder; error shows a message; data renders the list.
The home page route
Section titled “The home page route”import PostFeed from '../components/PostFeed'
export default function Home() { return ( <main> <h1>Bulletin</h1> <PostFeed /> </main> )}Register the route in your router:
// src/App.tsx (routes excerpt)<Route path="/" element={<Home />} />What useEffect with an empty dependency array means
Section titled “What useEffect with an empty dependency array means”useEffect(() => { // runs once — after the first render}, [])The empty array tells React: run this effect once, when the component mounts. It’s the right tool for fetching data that the page needs on load. You don’t want to re-fetch on every render — only when the component first appears.
Exercise
Section titled “Exercise”Open src/components/PostFeed.tsx in your Bulletin project.
- Add the
getPostsfunction tosrc/api/postsApi.tswith thePostinterface as shown above. - Create
PostFeed.tsxusing the three-state pattern (loading, error, data). - Create
src/pages/Home.tsxand render<PostFeed />inside it. - Register the
/route inApp.tsx. - Start the dev server (
npm run dev), start the backend, and confirm the feed displays real posts from the database. If the database is empty, seed it with one or two posts via the API (use Thunder Client or your terminal).
Hold onto PostFeed.tsx — you’ll add an upvote button to it in Lesson 05.
getPosts()callsGET /postsand returns a typed array.useEffect(() => { ... }, [])fetches once on mount.- The three-state pattern handles loading, error, and success cleanly.
- Each post links to
/posts/:id— you’ll build that page in Lesson 03.