TutorialsPreview
LearnBuildintermediate40 min3 steps

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.

You will build

A signup endpoint that accepts an email and password, hashes the password with bcrypt, and writes a row to a users table.

You will learn

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.

01

Step 18 min

A users table

GoalA migration that creates a `users` table with id, email, password_hash. Nothing more.

migrations/0001_users.sqlSQL
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()
);
02

Step 210 min

Hash a password

GoalA function that takes a plain password and returns a hash. We will use bcrypt.

Concept · auth/password-hashing
Catalog

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.

A good password hashing function is also slow on purpose — a brute-force attacker has to spend real CPU per guess. bcrypt, argon2, and scrypt are the three you should ever pick from. SHA-256 is a hash function but not a password hasher — it is too fast.
TYPESCRIPT
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash('correct horse battery staple', 12);
bash
$pnpm add bcrypt
+ bcrypt@5.x — added
src/auth/passwords.tsTYPESCRIPT
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);
}
03

Step 315 min

The signup endpoint

GoalPOST /auth/signup accepts { email, password }, hashes the password, inserts the user, returns the new id.

src/auth/signup.tsTYPESCRIPT
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.

Concepts covered · 3

More like this

M
Build·beginner·12 min

Make a page respond: a click counter

A page becomes alive the moment JavaScript listens for an event and changes the page in response. Build a counter, then open it in /code.

B
Build·beginner·9 min

Build interaction with AI

The vibe-coder path for JS events: the event→state→render loop you must hold in your head, the prompt, and the review.

B
Build·beginner·8 min

Build a script with AI

The vibe-coder path for Python: the shape of a small program, the prompt, and the edge cases AI forgets.

Liked this one?

Pass it on

Discussion

Be the first to ask

Your questions stay private with the team. We can pin answers to share with everyone.

Sign in to ask a question privately on this tutorial.

Sign in

No pinned answers yet for this tutorial.