TutorialsMetals API › File Structure

Metals API · Section 3 of 3

How the Code Is Organized

metals_api/ is split into layers, each with one job: routes handle HTTP, services hold business rules, repositories talk to the database, and models describe the tables. DTOs sit at the edges, shaping what comes in and goes out as JSON. Nothing here changed structurally to add authentication — it just added one more resource (auth) that follows the same pattern as everything else, plus one shared file, auth.py, that every route can use.

Codey the Sr Developer, standing with a pointer, ready to walk through the guide
Folder → metals_api/ Pattern → routes → services → repositories → models ORM → SQLAlchemy Tests mirror → the same folders, under tests/

Purpose of this page

Show what each file and folder in metals_api/ is responsible for, and how a request actually moves through them.

Codey typing on a laptop
one job per layer

The folder, top to bottom structure

File / folderWhat it's for, in plain terms
app.pyCreates the Flask app, reads JWT_SECRET_KEY / JWT_EXPIRATION_MINUTES from the environment, registers every blueprint (auth, elements, alloys, alloy elements, coins), and defines the one route that isn't in a blueprint: /health. The module-level app object it creates is what Gunicorn serves in the container.
DockerfileBuilds the metals-api container image — Python 3.11, the dependencies, this folder, and Gunicorn on port 5000. Its build context is the repository root, not this folder. (dig deeper)
auth.pyThe two decorators covered in Section 2: require_auth and require_roles. Shared by every route file below.
database/connection.pyBuilds the SQLAlchemy engine and SessionFactory from the DB_* variables in .env.
models/One SQLAlchemy class per table (Element, Alloy, AlloyElement, Coin, User, Role), plus user_role.py for the users↔roles join table and base.py for the shared declarative base.
dtos/Plain dataclasses shaping request and response JSON — a Create*DTO, Update*DTO, and *ResponseDTO per resource, each with its own validate(). base.py holds the shared to_dictionary / from_dictionary conversion logic.
repositories/Plain functions that run the actual SQLAlchemy queries — get, add, update, delete — one file per table. user_repository.py is what auth_service.py uses to look up accounts and roles.
services/Business logic: opens a session, calls the repository, enforces rules (like “no duplicate element name”), and converts between models and DTOs. auth_service.py additionally hashes/verifies passwords with Argon2id and issues JWTs. exceptions.py defines BusinessValidationError, the one exception every service raises for a rule violation.
routes/Flask Blueprints — one per resource — wiring HTTP verb + path to a service call, applying @require_auth / @require_roles, and turning DTO validation errors or BusinessValidationError into the JSON error shapes from Section 1.
tests/A pytest suite mirroring the folders above (models/, dtos/, repositories/, services/, routes/), plus conftest.py, which sets test-only environment variables and a make_token() helper for building a valid JWT without actually calling /api/auth/login.

Every file in routes/, services/, repositories/, and models/ follows the same naming pattern: <resource>_routes.py, <resource>_service.py, and so on. Once you understand one resource end to end, all five (including auth) read the same way.

Two more files live at the repository root rather than in this folder, because the image build needs them there: requirements-container.txt, which pulls in requirements.txt and adds Gunicorn, and .dockerignore, which restricts what the build is even allowed to see — notably excluding tests/ and any .env file.

One request, traced through every layer POST /api/elements

The clearest way to see how these layers connect is to follow one real request all the way through — an Admin creating a new element.

Codey pointing to the left
four layers, one request
1. Routeroutes/element_routes.py
@element_blueprint.post("")
@require_auth
@require_roles("Admin")
def create_element():
    data = request.get_json(silent=True)
    errors = CreateElementDTO.validate(data)
    if errors:
        return jsonify({"errors": errors}), 400

    request_dto = CreateElementDTO.from_dictionary(data)
    response_dto = service.create_element(request_dto)
    return jsonify(response_dto.to_dictionary()), 201

require_auth and require_roles already ran (see Section 2). The route's own job is small: validate the raw JSON with the DTO, turn it into a typed CreateElementDTO, and hand it to the service.

2. DTOdtos/element_dto.py

CreateElementDTO.validate(data) checks required fields, types, string lengths, and value ranges — all before a database session is even opened. from_dictionary also converts numeric strings into Decimal for fields like density, so the service never has to think about that conversion.

3. Serviceservices/element_service.py
def create_element(dto: CreateElementDTO) -> ElementResponseDTO:
    with SessionFactory() as session:
        if element_repository.get_element_by_id(session, dto.atomic_number):
            raise BusinessValidationError(["An element with this atomic_number already exists."])
        ...
        element = Element(**dto.to_dictionary())
        element = element_repository.add_element(session, element)
        return ElementResponseDTO.from_model(element)

Opens a database session, enforces business rules the DTO can't know about by itself (like uniqueness, which requires checking the database), builds a Element model instance, and asks the repository to save it.

4. Repository & Modelrepositories/element_repository.py, models/element.py

The repository's add_element is a thin wrapper around session.add(element) / session.commit() — it doesn't know or care why it's being asked to save this row. Element, the model, is what SQLAlchemy uses to translate that Python object into the actual INSERT statement against the elements table described in the Database schema tutorial.

Back up the stackresponse

ElementResponseDTO.from_model(element) converts the saved model back into a response DTO, the route calls .to_dictionary() on it and wraps it in jsonify(...), 201 — the JSON body an Insomnia request would actually see.

Key terms glossary

Blueprint
Flask's way of grouping related routes together (all of /api/elements, for example) and registering them on the app as one unit in app.py.
DTO (Data Transfer Object)
A plain object whose only job is describing the shape of data crossing a boundary — here, JSON in and out of the API. DTOs validate; models don't.
Repository
A layer whose only job is running database queries for one table, so services never write raw SQLAlchemy queries directly.
Session / SessionFactory
SQLAlchemy's unit of work with the database — a session is opened, used for one logical operation, and closed. SessionFactory() creates a new one each time a service needs one.
Model
A Python class mapped to a database table by SQLAlchemy's ORM (Object-Relational Mapper) — an instance of Element corresponds to one row in the elements table.
Codey giving a thumbs up

All three sections done — endpoints, authentication, and the code behind both. Next, see who actually calls these endpoints in the Metals UI tutorial, or head back to the tutorials home any time.