CodingNic

Password Hashing & User Accounts

A User Model and Registration Endpoint

Password Hashing & User Accounts 15 min read

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

text
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.

javascript
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

javascript
// 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

javascript
// 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);
})();
bash
node test.js
text
201 { id: 1, email: 'erin@example.com' }

Handling a Duplicate

javascript
const res2 = await request(app).post('/api/v1/auth/register').send({ email: 'erin@example.com', password: 'different' });
console.log(res2.status, res2.body);
text
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

javascript
const res3 = await request(app).post('/api/v1/auth/register').send({ email: 'jordan@example.com' });
console.log(res3.status, res3.body);
text
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

  1. Build this registration endpoint, and confirm all three responses (success, duplicate, missing field) match exactly.
  2. Log the users array after a successful registration, and confirm it contains a passwordHash, never a password.
  3. 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.
  4. Explain, in your own words, why the response to a successful registration excludes passwordHash entirely.

Recap

  • A user is stored with a passwordHash, never a password, 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 Conflict is the correct status code for a duplicate registration, distinct from 400 for malformed input.

This is the final lesson of this module before exercises. Next module: turning a registered user into a logged-in session.