User Registration and Login Endpoints
The auth endpoints are the entry point to Bulletin. Every user starts here. Building them well means getting registration, login, and token issuance right.
The auth router
Section titled “The auth router”import { Router } from 'express'import { register, login } from '../controllers/auth.js'import { requireFields } from '../middleware/validate.js'import rateLimit from 'express-rate-limit'
const router = Router()
const authLimiter = rateLimit({ windowMs: 60 * 60 * 1000, max: 10, message: { error: 'Too many attempts, try again in an hour' },})
router.post('/register', authLimiter, requireFields('username', 'password'), register)router.post('/login', authLimiter, requireFields('username', 'password'), login)
export default routerRegistration controller
Section titled “Registration controller”import { Request, Response } from 'express'import bcrypt from 'bcrypt'import { db } from '../db/index.js'import { signToken } from '../utils/jwt.js'
const SALT_ROUNDS = 12
export async function register(req: Request, res: Response) { const { username, password } = req.body as { username: string; password: string }
// Validate username format if (!/^[a-zA-Z0-9_]{3,30}$/.test(username)) { return res.status(400).json({ error: 'Username must be 3–30 characters, letters/numbers/underscores only' }) }
// Validate password strength if (password.length < 8) { return res.status(400).json({ error: 'Password must be at least 8 characters' }) }
// Check if username is taken const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username) if (existing) { return res.status(409).json({ error: 'Username already taken' }) }
// Hash the password const passwordHash = await bcrypt.hash(password, SALT_ROUNDS)
// Create the user const result = db.prepare( 'INSERT INTO users (username, password_hash) VALUES (?, ?)' ).run(username, passwordHash)
const userId = result.lastInsertRowid as number
// Issue JWT const token = signToken({ userId, username })
res.status(201).json({ token, userId, username })}Login controller
Section titled “Login controller”export async function login(req: Request, res: Response) { const { username, password } = req.body as { username: string; password: string }
// Find user const user = db.prepare( 'SELECT id, username, password_hash FROM users WHERE username = ?' ).get(username) as { id: number; username: string; password_hash: string } | undefined
// Use same message for both "user not found" and "wrong password" const invalid = () => res.status(401).json({ error: 'Invalid username or password' })
if (!user) return invalid()
// Verify password const isValid = await bcrypt.compare(password, user.password_hash) if (!isValid) return invalid()
// Issue JWT const token = signToken({ userId: user.id, username: user.username })
res.json({ token, userId: user.id, username: user.username })}Testing the auth endpoints
Section titled “Testing the auth endpoints”# Registercurl -X POST http://localhost:3000/auth/register \ -H "Content-Type: application/json" \ -d '{"username": "alice", "password": "securepassword"}'# → { "token": "eyJ...", "userId": 1, "username": "alice" }
# Logincurl -X POST http://localhost:3000/auth/login \ -H "Content-Type: application/json" \ -d '{"username": "alice", "password": "securepassword"}'# → { "token": "eyJ...", "userId": 1, "username": "alice" }
# Wrong passwordcurl -X POST http://localhost:3000/auth/login \ -H "Content-Type: application/json" \ -d '{"username": "alice", "password": "wrongpassword"}'# → 401 { "error": "Invalid username or password" }What the client does with the token
Section titled “What the client does with the token”The React frontend stores the token in localStorage or React state and includes it in subsequent requests:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...The API verifies this token with the authenticate middleware — no session table lookup needed.
Exercise
Section titled “Exercise”- Create
src/routes/auth.tsandsrc/controllers/auth.ts. - Mount the auth router in
src/index.ts. - Test registration: create two users. Confirm the second registration with the same username returns 409.
- Test login: correct credentials → 200 with token, wrong password → 401.
- Decode the returned token at jwt.io — inspect the payload.
POST /auth/registerhashes the password, inserts the user, returns a JWT.POST /auth/loginfinds the user, verifies the password with bcrypt, returns a JWT.- Both endpoints use the same error message for “not found” and “wrong password”.
- Rate limit auth endpoints — 10 attempts per hour prevents brute force.
- Input validation (username format, password length) belongs in the controller, not just the database.