TutorialsMetals API › The Endpoints

Metals API · Section 1 of 3

The API Endpoints

Five resources, all under /api, all speaking JSON. Four of them — elements, alloys, alloy elements, and coins — follow the exact same shape: list, get one, create, update, delete. The fifth, /api/auth, is how you get a token in the first place, covered in depth in Section 2. One route sits outside that pattern entirely: /health, with no /api prefix and no authentication.

Codey the Sr Developer, standing with a pointer, ready to walk through the guide
Base URL (local) → http://localhost:5000 Content type → application/json Resources → auth, elements, alloys, alloy-elements, coins

Purpose of this page

List every endpoint this API exposes, who's allowed to call it, and what a request and response look like for each.

Codey typing on a laptop
one shape, five resources

Resources at a glance the map

ResourceBase pathReading itChanging it
Auth/api/authpublic (register, login)n/a — see Section 2
Elements/api/elementsany logged-in userAdmin only
Alloys/api/alloysany logged-in userAdmin only
Alloy Elements/api/alloy-elementsany logged-in userAdmin only
Coins/api/coinsany logged-in userAdmin only

Every request other than /api/auth/register, /api/auth/login, and /health needs an Authorization: Bearer <token> header. Without one, every endpoint below responds 401 Unauthorized before any of its own logic runs — see Section 2 to get a token.

/health metals_api/app.py

The one route defined directly in app.py rather than a blueprint — no /api prefix, no authentication, and deliberately no database access. It returns {"status":"ok"} with 200 whenever the HTTP server is running.

Why it ignores the databasea deliberate choice

It answers one question: “is this process serving HTTP?” If it also checked the database, a brief database blip would mark the container unhealthy and trigger a restart — restarting a perfectly healthy API for a problem it can't fix. Deeper dependency checks belong in monitoring, not in a liveness probe.

Three things consume itone endpoint, reused
  • The image's own HEALTHCHECK, so Docker knows the container's status.
  • Compose's depends_on: condition: service_healthy, which holds the UI back until the API answers.
  • Azure's healthCheckPath, and the /health polling every deployment path does before declaring success.

The UI container exposes its own /health too — answered by Nginx directly, with no application code involved.

/api/elements metals_api/routes/element_routes.py

Chemical elements, keyed by atomic number.

Method & pathWho can call itWhat it does
GET /api/elementsany logged-in userList elements. Optional ?name= and ?color= query filters.
GET /api/elements/<atomic_number>any logged-in userGet one element, or 404 if it doesn't exist.
POST /api/elementsAdminCreate an element.
PUT /api/elements/<atomic_number>AdminUpdate one or more fields on an existing element.
DELETE /api/elements/<atomic_number>AdminDelete an element, or 404 if it doesn't exist.
Create request bodyPOST /api/elements
{
  "atomic_number": 3,
  "name": "Lithium",
  "symbol": "Li",
  "melting_point_f": 356.9,
  "boiling_point_f": 2447.6,
  "color": "silvery",
  "density": 0.53,
  "category": "ALKALI_METAL",
  "state_at_room_temp": "SOLID",
  "is_toxic": false,
  "is_magnetic": false,
  "common_uses": "batteries"
}

Only atomic_number, name, and symbol are required; everything else is optional. Sending an unrecognized field, or a field of the wrong shape (like a negative density, or a state_at_room_temp outside SOLID / LIQUID / GAS), returns 400 before anything is written — see how errors are reported below.

Successful response201 Created
{
  "atomic_number": 3,
  "name": "Lithium",
  "symbol": "Li",
  "melting_point_f": 356.9,
  "boiling_point_f": 2447.6,
  "color": "silvery",
  "density": 0.53,
  "category": "ALKALI_METAL",
  "state_at_room_temp": "SOLID",
  "is_toxic": false,
  "is_magnetic": false,
  "common_uses": "batteries"
}

/api/alloys metals_api/routes/alloy_routes.py

Named mixtures, like “Sterling Silver.”

Method & pathWho can call itWhat it does
GET /api/alloysany logged-in userList alloys. Optional ?name= and ?color= query filters.
GET /api/alloys/<alloy_id>any logged-in userGet one alloy, or 404.
POST /api/alloysAdminCreate an alloy. Requires name; color and description are optional.
PUT /api/alloys/<alloy_id>AdminUpdate one or more fields.
DELETE /api/alloys/<alloy_id>AdminDelete an alloy, or 404.
// POST /api/alloys
{ "name": "Fine Gold 24K", "color": "gold", "description": "High-purity investment gold." }

/api/alloy-elements metals_api/routes/alloy_element_routes.py

The join table between alloys and elements — what percentage of a given alloy each element makes up. This is the one resource identified by two path values instead of one, matching its composite primary key.

Method & pathWho can call itWhat it does
GET /api/alloy-elementsany logged-in userList, filterable by ?alloy_id= and/or ?atomic_number=.
GET /api/alloy-elements/<alloy_id>/<atomic_number>any logged-in userGet one composition row, or 404.
POST /api/alloy-elementsAdminAdd an element to an alloy with a given percent_of_alloy.
PUT /api/alloy-elements/<alloy_id>/<atomic_number>AdminChange the percent_of_alloy for that pair.
DELETE /api/alloy-elements/<alloy_id>/<atomic_number>AdminRemove that element from that alloy.
// POST /api/alloy-elements
{ "alloy_id": 6, "atomic_number": 47, "percent_of_alloy": 92.5 }

/api/coins metals_api/routes/coin_routes.py

Real-world coins, each minted from exactly one alloy.

Method & pathWho can call itWhat it does
GET /api/coinsany logged-in userList, filterable by ?name=, ?country=, and/or ?alloy_id=.
GET /api/coins/<coin_id>any logged-in userGet one coin, or 404.
POST /api/coinsAdminCreate a coin. Requires name and alloy_id (must reference an existing alloy).
PUT /api/coins/<coin_id>AdminUpdate one or more fields.
DELETE /api/coins/<coin_id>AdminDelete a coin, or 404.
// POST /api/coins
{
  "name": "American Gold Eagle (1 oz)",
  "country": "United States",
  "mint": "United States Mint",
  "year_introduced": 1986,
  "alloy_id": 2,
  "gross_weight_g": 33.9305,
  "face_value": 50.00,
  "face_value_currency_code": "USD"
}

How errors are reported consistent across every endpoint

Every route validates the request before touching the database, then lets the service layer enforce business rules. Both layers report problems the same way, so client code only needs to handle one shape of error response.

Codey inspecting something with a magnifying glass
one error shape, four causes
StatusBody shapeWhen it happens
400 Bad Request{"errors": ["..."]}The request body failed DTO validation (missing/invalid fields), or a business rule was broken (e.g. a duplicate element name).
401 Unauthorized{"error": "..."}No Authorization header, or the token is missing/invalid/expired. See Section 2.
403 Forbidden{"error": "You do not have permission to perform this action."}You're logged in, but your token's roles don't include Admin, and this is a write endpoint.
404 Not Found{"error": "... not found"}The requested id (or id pair, for alloy elements) doesn't exist.
204 No Content(empty body)A DELETE succeeded.

Notice the field name differs by cause: validation and business-rule failures use errors (plural, an array — there can be more than one), while authentication, authorization, and not-found failures use error (singular, a string).

Codey giving a thumbs up

Endpoints covered — next, actually getting a token so you can call one of them.