Skip to content

About

Build a complete REST API to manage an address book using GET, POST, PUT and DELETE. The challenge is pure backend: validate the API with Postman and do not build a frontend.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Contacts API

A Dockerized REST API for a personal contacts / address book, built with FastAPI. It provides authenticated CRUD operations with JWT while keeping the architecture small and focused.

Built by Jatniel Guzmán (jatniel.dev) for the DevChallenges — Week 34 organized by YoanDev.

Challenge Brief

Build a complete REST API to manage an address book using GET, POST, PUT and DELETE. The challenge is pure backend: validate the API with Postman and do not build a frontend.

Overview

The API lets a user authenticate and then create, list, read, replace and delete their own contacts. Contacts are scoped to the authenticated user: nobody can see or modify someone else's contacts.

Features

  • OAuth2 password flow + JWT authentication (Argon2 password hashing)
  • Full CRUD for contacts (GET, POST, PUT, DELETE)
  • Per-user data isolation
  • Case-insensitive search and offset / limit pagination
  • SQLModel + SQLite with Alembic migrations
  • Automated tests (≥ 90% coverage)
  • Ruff formatting and linting
  • Docker + Docker Compose, one service
  • GitHub Actions CI

Tech Stack

Concern Choice
Language Python 3.13
Framework FastAPI
ORM / data layer SQLModel
Database SQLite (synchronous access)
Migrations Alembic
Authentication OAuth2 Password flow + JWT (PyJWT)
Password hashing pwdlib (Argon2)
Configuration pydantic-settings
Testing pytest + FastAPI TestClient
Lint / format Ruff
Container Docker / Docker Compose
CI GitHub Actions

Architecture / Project Structure

.
├── app/
│   ├── main.py                # FastAPI app, lifespan (demo seed)
│   ├── api/
│   │   ├── dependencies.py    # session + current-user dependencies
│   │   └── v1/
│   │       ├── router.py      # /api/v1 router
│   │       ├── auth.py        # POST /auth/token
│   │       └── contacts.py    # contact schemas + CRUD endpoints
│   ├── core/
│   │   ├── config.py          # pydantic-settings
│   │   └── security.py        # password hashing + JWT
│   ├── db/
│   │   ├── session.py         # engine + session dependency
│   │   └── seed.py            # idempotent demo-user seed
│   └── models/
│       ├── user.py
│       └── contact.py
├── alembic/                   # migrations
├── tests/                     # pytest suite, including SQLite FK coverage
├── postman/                   # Postman collection
├── .github/workflows/tests.yml
├── compose.yaml
├── Dockerfile
├── requirements.txt
├── requirements-dev.txt
└── pyproject.toml

API Endpoints

Method Path Auth Description
POST /api/v1/auth/token public Obtain a JWT
GET /api/v1/contacts Bearer List own contacts
POST /api/v1/contacts Bearer Create a contact
GET /api/v1/contacts/{id} Bearer Read one contact
PUT /api/v1/contacts/{id} Bearer Replace a contact
DELETE /api/v1/contacts/{id} Bearer Delete a contact
GET /health public Health check

List contacts supports offset (default 0), limit (default 20, max 100) and q (case-insensitive search over first name, last name, email and company).

Authentication

POST /api/v1/auth/token accepts an OAuth2 form body. The demo email goes in the username field:

curl -X POST http://localhost:8000/api/v1/auth/token \
  -d "username=demo@example.com&password=demo-password"

The response is a JWT used as a bearer token:

{ "access_token": "...", "token_type": "bearer" }

Demo credentials (clearly demo values, configurable via environment variables):

  • email: demo@example.com
  • password: demo-password

Quick Start with Docker

Only Docker and Docker Compose are required. No local Python install is needed.

  1. Create the local configuration file:
cp .env.example .env
  1. Generate a secret and paste its output after JWT_SECRET_KEY= in .env:
openssl rand -hex 32

For example, if the command returns abc..., set JWT_SECRET_KEY=abc.... Do not leave this value empty and do not commit .env.

  1. Start the API:
docker compose up --build

The container applies Alembic migrations on startup, then serves the API. Open:

Stop with:

docker compose down

SQLite data is persisted in ./data via a bind mount.

Local Development

All Python commands run inside the container:

docker compose run --rm api pytest
docker compose run --rm api pytest --cov=app --cov-report=term-missing --cov-fail-under=90
docker compose run --rm api ruff check .
docker compose run --rm api ruff format .
docker compose run --rm api alembic upgrade head

Environment Variables

Copy .env.example to .env, then replace the empty JWT_SECRET_KEY value with a unique value of at least 32 characters. Generate one with openssl rand -hex 32 and never commit .env.

Variable Description Example
APP_NAME Application name Contacts API
ENVIRONMENT Runtime environment development
DATABASE_URL SQLite URL sqlite:///./data/contacts.db
JWT_SECRET_KEY Secret used to sign JWTs (see .env.example)
JWT_ALGORITHM JWT signing algorithm HS256
ACCESS_TOKEN_EXPIRE_MINUTES Token lifetime in minutes 30
DEMO_USER_EMAIL Seeded demo user email demo@example.com
DEMO_USER_PASSWORD Seeded demo user password demo-password

Database Migrations

Alembic is the source of truth for schema changes (no create_all() at startup).

docker compose run --rm api alembic revision --autogenerate -m "describe change"
docker compose run --rm api alembic upgrade head
docker compose run --rm api alembic current

Testing

docker compose run --rm api pytest --cov=app --cov-report=term-missing --cov-fail-under=90

Tests use an isolated in-memory SQLite database (StaticPool) and FastAPI dependency overrides, so the development database is never touched.

Postman

The repository includes a ready-to-use collection at postman/contacts-api.postman_collection.json.

  1. Start the API with docker compose up.
  2. In Postman, import postman/contacts-api.postman_collection.json.
  3. Run Auth / Login with the demo credentials, or the values configured in your .env file. The request stores the returned JWT in the access_token collection variable automatically.
  4. Run Contacts / List to view your contacts. An empty array is expected for a new demo user.
  5. Run Contacts / Create to add a contact. The request stores the returned identifier in the contact_id collection variable automatically.
  6. Run Contacts / Get, Contacts / Update or Contacts / Delete to manage that contact. These requests reuse both variables automatically, so no token or contact ID needs to be copied manually.

Contacts / List also accepts optional offset, limit and q query parameters. Use q to search case-insensitively by first name, last name, email or company.

The collection includes the following requests:

Folder Request Purpose
Root Health Check that the API is running
Auth Login Obtain and store a JWT
Contacts List Read all contacts belonging to the authenticated user
Contacts Create Create a contact and store its ID
Contacts Get Read the stored contact by ID
Contacts Update Replace the stored contact data
Contacts Delete Permanently delete the stored contact

Code Quality

docker compose run --rm api ruff format --check .
docker compose run --rm api ruff check .

Technical Decisions

  • Why FastAPI? Strong fit for typed APIs: validation, dependency injection and OpenAPI docs out of the box.
  • Why SQLModel? Combines SQLAlchemy-style persistence with Pydantic-style models, and integrates naturally with FastAPI.
  • Why SQLite? The challenge is a small, self-contained API. SQLite keeps the demo trivial to run without extra infrastructure.
  • Why synchronous DB access? Local SQLite gives no benefit from async I/O. Sync code is simpler and avoids fake concurrency complexity.
  • Why Alembic if SQLite is simple? To demonstrate controlled schema evolution instead of recreating tables from ORM metadata at runtime.
  • Why JWT? It demonstrates a realistic protected API while keeping authentication intentionally minimal.
  • Why no service/repository layers? For a small CRUD API, extra layers add indirection without enough value. This is an intentional choice, not an omission; the structure can evolve if business logic grows.

Possible Improvements

  • User registration and forgot-password flow
  • PATCH for partial updates and refresh tokens
  • Tags / categories on contacts
  • Rate limiting
  • PostgreSQL for heavier workloads

Author

License

This project is licensed under the MIT License. See LICENSE.

Copyright (c) 2026 Jatniel Guzmán.

About

Build a complete REST API to manage an address book using GET, POST, PUT and DELETE. The challenge is pure backend: validate the API with Postman and do not build a frontend.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages