13.4. Registration
Table of Contents
Understanding Registration in Backend Systems
User registration is usually the first contact between a user and your backend. It looks simple on the surface, but a good registration flow must balance user experience, security, data integrity, and future extensibility.
This chapter focuses only on what is unique to the registration step. Concepts such as password hashing, tokens, sessions, and email verification are explained in their own chapters, so we will reference them without going into full detail here.
What Registration Actually Does
At a high level, registration answers three questions:
- Who is this user?
Data such as email, username, display name, etc. - How will they prove their identity later?
A password, or an external identity provider (Google, GitHub, etc.). - What initial state should they start with?
Default roles, preferences, and system data such as creation timestamps.
When a user registers, your backend typically:
- Receives a registration request (HTTP POST).
- Validates the data.
- Checks that the user does not already exist.
- Hashes and stores the password (never plain text).
- Creates a user record in the database.
- Possibly creates related records (profile, settings).
- Optionally sends a verification email or a welcome message.
- Returns a response, maybe with an initial session or token.
You will combine registration with other authentication concepts in later chapters, but you should understand each of these steps on its own.
Typical Registration Data and Models
Common Registration Fields
Most basic registration forms contain:
| Field | Purpose | Example |
|---|---|---|
email | Unique identifier, contact method | alice@example.com |
password | Secret for future login | S3cur3P@ssw0rd |
password_confirm | Prevent typos | S3cur3P@ssw0rd |
username | Public identifier / handle | alice123 |
full_name | Display name | Alice Johnson |
accept_terms | Legal requirement in many products | true |
You might add optional data, for example:
localeorlanguagetimezonereferral_codemarketing_opt_in
For backend design, you decide which fields are:
- Required, for example
email,password. - Unique, for example
email,username. - Optional, for example
full_name.
Example User Table
A very common relational schema for a user table might look like:
| Column | Type | Constraints |
|---|---|---|
id | UUID or serial | Primary key |
email | text | Unique, not null |
password_hash | text | Not null |
username | text | Unique, not null |
full_name | text | Nullable |
is_active | boolean | Not null, default true |
is_verified | boolean | Not null, default false |
created_at | timestamp | Not null, default now() |
updated_at | timestamp | Not null, default now() |
The important part for registration is that you store the password as password_hash, not plain text. The details of hashing live in the "Password Hashing" chapter, but registration must call the hashing logic.
Designing a Registration API Endpoint
Basic Endpoint Shape
For a REST style API, registration is usually a POST request to an endpoint like:
POST /auth/registerPOST /api/v1/usersPOST /users/register
You choose one consistent convention and use it throughout your API.
Example HTTP request:
POST /auth/register HTTP/1.1
Content-Type: application/json
{
"email": "alice@example.com",
"password": "S3cur3P@ssw0rd",
"password_confirm": "S3cur3P@ssw0rd",
"username": "alice123"
}Example Request and Response Models
Imagine a Python style request model (simplified):
class RegisterRequest(BaseModel):
email: EmailStr
username: constr(min_length=3, max_length=30)
password: constr(min_length=8)
password_confirm: strYou can define a response model that never exposes the password:
class UserResponse(BaseModel):
id: UUID
email: EmailStr
username: str
is_verified: bool
created_at: datetimeA successful response might be:
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "c6e2a0dd-620f-4a63-8dd4-2c78b2df9cf1",
"email": "alice@example.com",
"username": "alice123",
"is_verified": false,
"created_at": "2026-08-27T10:45:00Z"
}Sometimes you also return an access token so the user is logged in immediately after registration. That logic is covered in login and tokens chapters, but registration can choose to call it.
Step-by-Step Registration Flow
1. Receive and Parse the Request
You read the HTTP body and validate that it is valid JSON or form data. The framework usually handles this.
@app.post("/auth/register", response_model=UserResponse, status_code=201)
def register_user(payload: RegisterRequest):
...
Your payload object now holds the user input.
2. Validate Input
Registration requires more than just type checking. Common validations:
password == password_confirm- Minimum password length
- Email format
- Username character set, for example letters, digits, underscores
- Required checkbox for terms of service
Example pseudo-code:
if payload.password != payload.password_confirm:
raise HTTPException(status_code=400, detail="Passwords do not match")
if len(payload.password) < 8:
raise HTTPException(status_code=400, detail="Password too short")
You can also check for forbidden usernames, for example admin, support, etc.
3. Check for Existing Users
Registration must guarantee uniqueness. Before creating the user, query the database:
existing_user = db.query(User).filter(User.email == payload.email).first()
if existing_user:
raise HTTPException(status_code=409, detail="Email already registered")
existing_username = db.query(User).filter(User.username == payload.username).first()
if existing_username:
raise HTTPException(status_code=409, detail="Username already taken")
Here 409 Conflict is a typical status code if the resource violates a uniqueness rule.
It is good practice to also enforce uniqueness with database constraints, not only in code, because concurrent requests can bypass only code checks.
4. Hash the Password
You never store payload.password directly. You pass it to your hashing function, for example:
password_hash = hash_password(payload.password)
Where hash_password uses a modern password hashing algorithm like bcrypt, Argon2, or scrypt.
Never store passwords in plain text.
Always hash passwords using a slow, salt based password hashing algorithm.
5. Create and Save the User Record
You build a new user entity:
user = User(
email=payload.email.lower(),
username=payload.username,
password_hash=password_hash,
is_verified=False,
is_active=True,
)
db.add(user)
db.commit()
db.refresh(user)Decisions made here:
- Lowercase the email for consistency.
- Set
is_verifiedtoFalseinitially if you plan email verification. - Set
is_activetoTrueunless you have an activation step.
If you use transactions, registration usually runs inside a single transaction so that either all changes are saved or none.
6. Create Related Data (Optional)
You can initialize extra data when creating a user, for example:
- User profile row
- Default settings row
- Initial quota or free trial
- Audit log entry like "User registered"
Example:
profile = UserProfile(user_id=user.id, bio="", avatar_url=None)
db.add(profile)
audit_log = AuditLog(
user_id=user.id,
action="user_registered",
ip_address=request.client.host,
)
db.add(audit_log)
db.commit()All of this still belongs to the registration process.
7. Optional: Send Verification or Welcome Communication
If your system requires email verification, registration usually triggers sending a verification email. The details are in "Email Verification" and "Email" chapters, but the registration endpoint might do something like:
verification_token = create_email_verification_token(user.id)
send_verification_email(user.email, verification_token)Or schedule the email as a background job.
8. Build and Return the Response
Finally, you convert your user model to a safe response and send it:
return UserResponse.from_orm(user)The HTTP status is usually:
201 Createdon success.400 Bad Requestfor invalid data.409 Conflictfor duplicate email or username.
Common Registration Patterns and Variations
1. Immediate Login After Registration
Many applications automatically log the user in after registration. The registration endpoint then returns not only the user, but also an access token.
Example response body:
{
"user": {
"id": "c6e2a0dd-620f-4a63-8dd4-2c78b2df9cf1",
"email": "alice@example.com",
"username": "alice123",
"is_verified": false
},
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}Token creation and validation are part of tokens and JWT chapters, but registration decides whether to call that logic.
2. Double Opt-In Registration
Some systems require that the user:
- Registers with email and password.
- Clicks a link in a verification email.
- Only then becomes fully active.
Flow:
- On registration, create user with
is_verified = False. - Send verification email with unique token.
- Verification endpoint later sets
is_verified = True.
Registration itself still stores the user, it just restricts what unverified users can do.
3. Social / Third Party Registration
When users register using Google, GitHub, or another provider, registration logic changes slightly:
- You do not ask for a password.
- You store the external provider ID.
- You may still require the user to choose a username.
Example user record for social login:
| Column | Example |
|---|---|
email | alice@example.com |
password_hash | NULL |
provider | google |
provider_id | 1234567890abcdef |
The registration endpoint in this case is triggered after the OAuth flow finishes, not by a simple HTML form. The concept of registration, however, is the same: create a new user record and set initial state.
Registration Validation Rules
Good validation catches errors early and improves security.
Email Validation
You usually check:
- Format looks like an email.
- It is not already in use.
Do not rely only on syntax validation. Real verification is done by sending a verification email.
Password Rules
Typical password constraints:
- Minimum length, for example 8 or 12 characters.
- Might require a combination of letters and numbers.
- Might block extremely common passwords.
You might use a function:
def validate_password(password: str) -> None:
if len(password) < 8:
raise ValueError("Password must be at least 8 characters")
if password.isdigit():
raise ValueError("Password cannot be all digits")
# More checks...Username Rules
Usernames often need:
- Minimum and maximum length.
- Only certain characters, for example letters, digits, underscore.
- No offensive or reserved words.
You can use a regular expression check:
USERNAME_REGEX = r"^[a-zA-Z0-9_]{3,30}$"Terms and Privacy Consent
If your form has a checkbox like "I accept the Terms of Service":
class RegisterRequest(BaseModel):
...
accept_terms: boolThen validate:
if not payload.accept_terms:
raise HTTPException(status_code=400, detail="Terms must be accepted")Handling Registration Errors
Common Error Types
Here are typical error conditions and how to respond.
| Condition | HTTP Status | Example Message |
|---|---|---|
| Invalid JSON or missing fields | 400 | "Invalid request body" |
| Passwords do not match | 400 | "Passwords do not match" |
| Password too weak | 400 | "Password must be at least 8 characters" |
| Email already registered | 409 | "Email already registered" |
| Username already taken | 409 | "Username already taken" |
| Terms not accepted | 400 | "Terms must be accepted" |
| Database constraint violation | 500 or 409 | "Could not create user" |
You can map specific validation errors to error codes in your API specification.
Generic vs Detailed Errors
You must find a balance:
- Too detailed responses can leak information, for example telling an attacker that an email already exists.
- Too generic responses frustrate normal users.
Many systems choose:
- During login: generic
"Invalid credentials". - During registration: explicit
"Email already registered"so the user knows they already have an account.
You can also rate limit registration attempts to prevent abuse, which is covered in rate limiting and security chapters.
Security Considerations in Registration
Registration is part of your security boundary.
Key considerations:
- Password handling
- Only accept passwords via secure connections (HTTPS).
- Never log passwords.
- Erase or overwrite raw passwords in memory as early as possible in low level languages.
- Rate limiting
- Limit how often a single IP can hit the registration endpoint.
- This reduces spam registrations and brute force attacks on email enumeration.
- CAPTCHA or similar
- Optional measure to slow down bots that create fake accounts.
- Email verification
- Prevents attackers from using someone else’s email.
- Reduces spam and bounced emails.
- Input validation
- Prevent injection attacks through fields like
usernameorfull_name.
Registration must never expose raw passwords, must never store passwords without hashing, and must carefully validate and sanitize all input.
Example End-to-End Registration Flow in Pseudocode
To put everything together, here is a simplified end-to-end registration handler.
@app.post("/auth/register", response_model=UserResponse, status_code=201)
def register_user(payload: RegisterRequest, db: Session = Depends(get_db)):
# 1. Basic validation
if payload.password != payload.password_confirm:
raise HTTPException(status_code=400, detail="Passwords do not match")
validate_password(payload.password)
# 2. Check if user already exists
if db.query(User).filter(User.email == payload.email.lower()).first():
raise HTTPException(status_code=409, detail="Email already registered")
if db.query(User).filter(User.username == payload.username).first():
raise HTTPException(status_code=409, detail="Username already taken")
# 3. Hash password
password_hash = hash_password(payload.password)
# 4. Create user
user = User(
email=payload.email.lower(),
username=payload.username,
password_hash=password_hash,
is_active=True,
is_verified=False,
)
db.add(user)
db.commit()
db.refresh(user)
# 5. Send verification email (optional)
token = create_email_verification_token(user.id)
send_verification_email(user.email, token)
# 6. Return safe user data
return userYou will later plug this handler into real password hashing, email verification, and database code. For now, focus on the responsibilities of the registration step itself: validate, ensure uniqueness, securely store, initialize state, and respond clearly.
Summary
Registration is the process where you:
- Collect user identity data.
- Validate and normalize input.
- Check uniqueness of email and username.
- Hash and store passwords, never in plain text.
- Create user and possibly related records.
- Optionally send verification or log the user in.
- Return a safe response to the client.
It is the foundation for everything else in authentication and authorization, so designing it carefully, with both user experience and security in mind, is essential.
Views: 5
KAHIBARO