Skip to content

Comments Endpoints

Comments are nested under posts — their URLs reflect this relationship. This lesson adds the comments router and controller.

src/routes/comments.ts
import { Router } from 'express'
import { listComments, createComment } from '../controllers/comments.js'
import { authenticate } from '../middleware/authenticate.js'
import { requireFields } from '../middleware/validate.js'
const router = Router({ mergeParams: true }) // inherit :id from parent router
router.get('/:id/comments', listComments)
router.post('/:id/comments', authenticate, requireFields('body'), createComment)
export default router

{ mergeParams: true } merges the parent router’s params into this router — necessary when nesting routers.

src/controllers/comments.ts
import { Request, Response } from 'express'
import { db } from '../db/index.js'
interface CommentRow {
id: number; body: string; user_id: number
post_id: number; created_at: string; author_username: string
}
// GET /posts/:id/comments
export function listComments(req: Request, res: Response) {
const postId = parseInt(req.params.id)
// Verify the post exists first
const post = db.prepare('SELECT id FROM posts WHERE id = ?').get(postId)
if (!post) return res.status(404).json({ error: 'Post not found' })
const comments = db.prepare(`
SELECT comments.*, users.username AS author_username
FROM comments
JOIN users ON comments.user_id = users.id
WHERE comments.post_id = ?
ORDER BY comments.created_at ASC
`).all(postId) as CommentRow[]
res.json(comments)
}
// POST /posts/:id/comments
export function createComment(req: Request, res: Response) {
const postId = parseInt(req.params.id)
const { body } = req.body as { body: string }
const { userId } = res.locals.user
// Verify the post exists
const post = db.prepare('SELECT id FROM posts WHERE id = ?').get(postId)
if (!post) return res.status(404).json({ error: 'Post not found' })
const result = db.prepare(
'INSERT INTO comments (body, user_id, post_id) VALUES (?, ?, ?)'
).run(body.trim(), userId, postId)
const comment = db.prepare(`
SELECT comments.*, users.username AS author_username
FROM comments
JOIN users ON comments.user_id = users.id
WHERE comments.id = ?
`).get(result.lastInsertRowid) as CommentRow
res.status(201).json(comment)
}
src/index.ts
import commentsRouter from './routes/comments.js'
app.use('/posts', commentsRouter)
// Results in: GET /posts/:id/comments and POST /posts/:id/comments

While we’re at it, add a basic user profile endpoint:

src/routes/users.ts
import { Router } from 'express'
import { db } from '../db/index.js'
const router = Router()
router.get('/:id', (req, res) => {
const userId = parseInt(req.params.id)
const user = db.prepare(
'SELECT id, username, bio, created_at FROM users WHERE id = ?'
).get(userId) as { id: number; username: string; bio: string | null; created_at: string } | undefined
if (!user) return res.status(404).json({ error: 'User not found' })
const { total: postCount } = db.prepare(
'SELECT COUNT(*) as total FROM posts WHERE user_id = ?'
).get(userId) as { total: number }
res.json({ ...user, postCount })
})
export default router
Terminal window
# List comments
curl http://localhost:3000/posts/1/comments
# Add a comment
curl -X POST http://localhost:3000/posts/1/comments \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "Great post!"}'
  1. Create src/routes/comments.ts and src/controllers/comments.ts.
  2. Mount both routers in src/index.ts.
  3. Create a post, then add two comments, then list them.
  4. Try adding a comment to a non-existent post — confirm 404.
  • Comments are nested under posts: GET /posts/:id/comments, POST /posts/:id/comments.
  • Use { mergeParams: true } when the nested router needs the parent’s route params.
  • Always verify the parent resource (post) exists before operating on its children (comments).
  • ORDER BY created_at ASC for comments — show oldest first (chronological thread order).