Creating a New Post
Authenticated users can create posts. The form collects a title and body, submits to POST /posts, and redirects to the new post’s detail page on success.
The createPost API call
Section titled “The createPost API call”// src/api/postsApi.ts (add to existing file)export async function createPost(data: { title: string; body: string }): Promise<Post> { const res = await apiClient.post<Post>('/posts', data) return res.data}The axios interceptor from Module 09 attaches the Authorization header automatically — this call doesn’t need to do it manually.
The CreatePost page
Section titled “The CreatePost page”import { useState, FormEvent } from 'react'import { useNavigate } from 'react-router-dom'import { createPost } from '../api/postsApi'
export default function CreatePost() { const [title, setTitle] = useState('') const [body, setBody] = useState('') const [error, setError] = useState<string | null>(null) const [submitting, setSubmitting] = useState(false) const navigate = useNavigate()
async function handleSubmit(e: FormEvent) { e.preventDefault() if (!title.trim() || !body.trim()) { setError('Title and body are required.') return } setSubmitting(true) setError(null) try { const post = await createPost({ title, body }) navigate(`/posts/${post.id}`) } catch { setError('Failed to create post. Please try again.') } finally { setSubmitting(false) } }
return ( <main> <h1>New Post</h1> {error && <p style={{ color: 'red' }}>{error}</p>} <form onSubmit={handleSubmit}> <div> <label htmlFor="title">Title</label> <input id="title" value={title} onChange={e => setTitle(e.target.value)} required /> </div> <div> <label htmlFor="body">Body</label> <textarea id="body" value={body} onChange={e => setBody(e.target.value)} rows={6} required /> </div> <button type="submit" disabled={submitting}> {submitting ? 'Posting...' : 'Post'} </button> </form> </main> )}Wiring the protected route
Section titled “Wiring the protected route”Creating a post requires authentication. Wrap the route with ProtectedRoute:
// src/App.tsx (routes excerpt)<Route path="/create" element={ <ProtectedRoute> <CreatePost /> </ProtectedRoute> }/>An unauthenticated user who visits /create is redirected to /login, then back to /create after logging in — the redirect flow built in Module 09 handles this automatically.
Adding a “New Post” link
Section titled “Adding a “New Post” link”Put a link in your navigation so logged-in users can reach the form:
// src/components/Nav.tsx (excerpt)import { useAuth } from '../context/AuthContext'
const { isAuthenticated } = useAuth()
// in JSX:{isAuthenticated && <Link to="/create">New Post</Link>}Only show the link if the user is authenticated — a nice UI guard on top of the route-level guard.
Client-side validation vs server-side validation
Section titled “Client-side validation vs server-side validation”The form checks that title and body are non-empty before submitting. The server also validates these — the form check is a fast fail to give immediate feedback, not a replacement for server validation. Never rely solely on frontend checks: a user can bypass them by calling the API directly.
Exercise
Section titled “Exercise”Open src/pages/CreatePost.tsx in your Bulletin project.
- Add
createPosttosrc/api/postsApi.ts. - Create
CreatePost.tsxwith controlled inputs for title and body, a submit handler, and error/submitting state. - Wrap the
/createroute inProtectedRouteinApp.tsx. - Add a “New Post” link to your
Navcomponent that only shows whenisAuthenticatedis true. - Log in as a test user, navigate to
/create, fill in the form, and submit. Confirm you land on the new post’s detail page (a 404 for now — that’s Lesson 03).
createPost()posts to/posts; the axios interceptor attaches the auth token.- Client-side validation provides fast feedback; server-side validation is the real gate.
- Wrap the
/createroute inProtectedRouteso unauthenticated users are redirected. navigate('/posts/' + post.id)redirects to the new post after creation.