A User Model and Registration Endpoint
Objectives
By the end of this lesson, you should be able to:
- Store a user with a hashed password, never a plain-text one
- Build a registration endpoint that validates and hashes correctly
- Handle a duplicate registration attempt correctly
๐ก Why this matters: The last lesson hashed a password in isolation. This lesson wires that into a real endpoint, the actual starting point of every authentication flow in the rest of this course.
โ ๏ธ A note on verification: every snippet and every output in this lesson was actually run, with real HTTP requests against a real Express app.
Project File Structure
registration-demo/
โโโ package.json
โโโ app.js
โโโ test.js
The User Store
This lesson uses a simple in-memory array to keep the focus on authentication itself, the same users array pattern from Node.js & Express Foundations. In a real project, this would be a User model backed by Prisma or Mongoose (Databases & ORMs for Node.js), storing exactly the same fields.
const users = [];
let nextId = 1;
Notice there’s no password field anywhere, only ever a passwordHash. That’s true throughout this entire course: a plain-text password exists in memory for the few milliseconds it takes to hash it, and never anywhere else, never in a variable named password, never logged, never stored.
The Registration Endpoint
// app.js
const express = require('express');
const bcrypt = require('bcryptjs');
const app = express();
app.use(express.json());
const users = [];
let nextId = 1;
app.post('/api/v1/auth/register', async (req, res, next) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'ValidationError', message: 'email and password are required' });
}
if (users.find(u => u.email === email)) {
return res.status(409).json({ error: 'Conflict', message: 'email already registered' });
}
const passwordHash = await bcrypt.hash(password, 10);
const user = { id: nextId++, email, passwordHash };
users.push(user);
res.status(201).json({ id: user.id, email: user.email });
} catch (err) {
next(err);
}
});
module.exports = app;
Reading through the checks, in order: both fields present, no existing account with that email, then, and only then, hash the password and store the user. The response never includes passwordHash, only id and email, a hash never needs to leave the server at all.
Testing It
// test.js
const request = require('supertest');
const app = require('./app');
(async () => {
const res1 = await request(app).post('/api/v1/auth/register').send({ email: 'erin@example.com', password: 'sunshine123' });
console.log(res1.status, res1.body);
})();
node test.js
201 { id: 1, email: 'erin@example.com' }
Handling a Duplicate
const res2 = await request(app).post('/api/v1/auth/register').send({ email: 'erin@example.com', password: 'different' });
console.log(res2.status, res2.body);
409 { error: 'Conflict', message: 'email already registered' }
409 Conflict (Node.js & Express Foundations, Module 9’s status code conventions) is the correct response here, the request was well-formed, but it conflicts with existing state, a different case than the 400 used for missing fields.
Handling Missing Fields
const res3 = await request(app).post('/api/v1/auth/register').send({ email: 'jordan@example.com' });
console.log(res3.status, res3.body);
400 { error: 'ValidationError', message: 'email and password are required' }
This manual check is exactly what Module 5’s validation library replaces with something more robust, it’s shown here in its simplest form so the underlying idea, reject bad input before touching the password at all, is clear before that library is introduced.
Try It
- Build this registration endpoint, and confirm all three responses (success, duplicate, missing field) match exactly.
- Log the
usersarray after a successful registration, and confirm it contains apasswordHash, never apassword. - Register a user, then try to register the exact same email with a different password, and confirm the second attempt is rejected without ever hashing the second password.
- Explain, in your own words, why the response to a successful registration excludes
passwordHashentirely.
Recap
- A user is stored with a
passwordHash, never apassword, the plain-text value only ever exists briefly, in memory, during hashing. - A registration endpoint checks required fields, checks for a duplicate, then hashes and stores, in that order.
409 Conflictis the correct status code for a duplicate registration, distinct from400for malformed input.
This is the final lesson of this module before exercises. Next module: turning a registered user into a logged-in session.