Build the signup half of a login system
Email and password. From the form to the database, with the hashing your future users will thank you for.
A signup endpoint that accepts an email and password, hashes the password with bcrypt, and writes a row to a users table.
Why you never store raw passwords, what a hashing function actually does, and the rhythm of a server-side write.
Prerequisites
A Node project with a database connection (Postgres works; SQLite is fine). Comfort writing async functions.
Auth is the part of the stack everyone uses and almost nobody has written. That is fine, libraries exist. But writing it once teaches you something the libraries hide: the moves you are trusting them to make on your behalf.
Step 18 min
A users table
GoalA migration that creates a `users` table with id, email, password_hash. Nothing more.
create table users ( id bigserial primary key, email varchar(320) not null unique, password_hash text not null, created_at timestamptz not null default now());Step 210 min
Hash a password
GoalA function that takes a plain password and returns a hash. We will use bcrypt.
Password hashing
A hash function maps a password to a fixed-size string that cannot be reversed back to the password. We store the hash; if our DB leaks, the attacker still does not have passwords.
import bcrypt from 'bcrypt';const hash = await bcrypt.hash('correct horse battery staple', 12);$pnpm add bcrypt+ bcrypt@5.x — added
import bcrypt from 'bcrypt'; const COST = 12; export function hashPassword(plain: string): Promise<string> { return bcrypt.hash(plain, COST);} export function verifyPassword(plain: string, hash: string): Promise<boolean> { return bcrypt.compare(plain, hash);}Step 315 min
The signup endpoint
GoalPOST /auth/signup accepts { email, password }, hashes the password, inserts the user, returns the new id.
import { db } from '../db';import { hashPassword } from './passwords'; interface SignupInput { email: string; password: string; } export async function signup(input: SignupInput) { if (!input.email.includes('@') || input.password.length < 8) { return { ok: false, error: 'invalid input' }; } const passwordHash = await hashPassword(input.password); try { const [row] = await db.insert('users', { email: input.email.toLowerCase(), password_hash: passwordHash, }).returning(['id']); return { ok: true, id: row.id }; } catch (err) { if (isUniqueViolation(err)) return { ok: false, error: 'email taken' }; throw err; }}Verify it works
Milestone
You wrote real auth.
You now know what `bcrypt.hash` does, why we lowercase the email, why the unique-violation check exists, and how the response shape protects callers from secrets. That knowledge survives every library you use later.
More like this
Build your first web page
Hand-write a small, semantic page — a heading, a paragraph, a list — then open the same project in /code and make it yours.
Build a calculator
Most courses teach variables before you have anything to put in one. We are going to skip that pain. We build the calculator first; the concepts show up when the calculator demands them.
Style a card with CSS
The quickest way to feel CSS: take a plain card and give it depth, spacing, and a button — editing the same project the Deep and Reference tiers use.
Liked this one?
Pass it on
Discussion
Be the first to ask
Your questions stay private with the author. The author can pin answers to share with everyone.
Sign in to ask a question privately on this tutorial.
Sign inNo pinned answers yet for this tutorial.