REST API 22 ๐ API Documentation with OpenAPI and Swagger
API documentation has a reputation problem: it is either absent, outdated, or maintained by hand until the first endpoint change makes it wrong. The OpenAPI Specification exists to solve this by making the API description a machine-readable artifact. The specificationโa YAML or JSON fileโdescribes every endpoint, parameter, request body, response schema, and authentication method. Tools then consume that file to render interactive documentation, generate client SDKs, and validate contracts in CI . Swagger is the toolset built around that specification: Swagger Editor for writing and validating the spec, Swagger UI for rendering it as interactive docs, and Swagger Codegen for generating code from it .
For the REST API series, the distinction between OpenAPI and Swagger is foundational. OpenAPI is the standard. Swagger is the implementation. Confusing the two leads to conversations where “Swagger file” and “OpenAPI spec” are used interchangeably, which obscures the fact that the spec can be consumed by tools that have nothing to do with SwaggerโRedoc, Stoplight Elements, Postman, and API gateways all read the same OpenAPI document . This chapter covers the specification structure, the code-first and design-first approaches to producing it, the rendering tools, and the CI integration that keeps documentation from drifting.
Key point: The OpenAPI document is the contract. Documentation, client libraries, and contract tests are all derived from it. Hand-editing the rendered documentation instead of the spec guarantees drift; editing the spec and regenerating guarantees consistency.
Why OpenAPI and Swagger exist
The documentation drift problem. Hand-written API documentation is a snapshot. The moment an endpoint changesโa field is renamed, a status code is adjusted, a query parameter is addedโthe documentation becomes misleading. OpenAPI solves this by making the specification the single source of truth. The rendered documentation is generated from the spec, so a change to the spec propagates to every consumer automatically . The discipline required is not “update the docs”; it is “update the spec.”
The tool fragmentation problem. Before OpenAPI, every API documentation tool used its own format. A specification written for one tool could not be read by another. The OpenAPI Specification standardized the description format, which allowed an ecosystem of tools to emerge: editors, renderers, code generators, mock servers, and contract validators all reading the same file . This standardization is why a team can switch from Swagger UI to Redoc without rewriting their API description.
The code-first versus design-first problem. Some teams write the specification first and generate server stubs from it. Others write the code first and generate the specification from annotations. Both approaches produce an OpenAPI document, but they differ in where the contract lives. Design-first treats the spec as the authoritative artifact and the code as an implementation of it. Code-first treats the code as authoritative and the spec as a derived artifact . Neither is universally correct; the choice depends on whether the API is being designed for external consumers or documented after the fact.
The validation problem. A specification that is syntactically valid can still be semantically wrong: a required field missing from a schema, a response code not documented, an authentication requirement absent from an endpoint. OpenAPI linters catch these issues in CI before the spec is published . Without automated validation, the specification becomes another artifact that is assumed to be correct until a consumer discovers it is not.
a. The OpenAPI Document Structure
An OpenAPI document has a small number of top-level sections. The openapi field declares the specification version. The info object contains the title, version, and description. The servers array lists the base URLs. The paths object is where endpoints are defined. The components object holds reusable schemas, security schemes, and parameters .
# A minimal OpenAPI 3.0 document
openapi: 3.0.3
info:
title: Orders API
version: 1.0.0
description: API for managing customer orders
servers:
- url: https://api.example.com/v1
description: Production
- url: http://localhost:3000/v1
description: Development
The paths object maps URL paths to operations. Each operation is identified by an HTTP method and includes a summary, parameters, request body, and responses. A response must document its status code, description, and content schema .
paths:
/orders/{id}:
get:
summary: Get an order by ID
operationId: getOrder
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: The order
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: Order not found
The $ref syntax in the response schema points to a reusable definition in components/schemas. This is how OpenAPI avoids duplication: a schema defined once can be referenced from every endpoint that returns that shape .
components:
schemas:
Order:
type: object
required: [id, total, status]
properties:
id:
type: string
total:
type: number
format: decimal
status:
type: string
enum: [pending, confirmed, shipped]
The version field at the root is the authoritative indicator of which specification version the document uses. A file named swagger.yaml can contain OpenAPI 3.x, and a file named openapi.json can contain version 2.0. The swagger: "2.0" field indicates version 2.0; the openapi: 3.x.x field indicates version 3.x .
b. Producing the Specification: Code-First and Design-First
The specification can be produced in two directions. In the design-first approach, the OpenAPI document is written before the code, and the code implements it. In the code-first approach, annotations in the code generate the specification.
FastAPI is the clearest example of code-first. It generates an OpenAPI document automatically from Pydantic models and endpoint decorators. The specification is available at /openapi.json and rendered at /docs by default . The schema for a request body comes from the Pydantic model, and the response schema comes from the response_model argument.
# FastAPI generates OpenAPI from type hints and Pydantic models
from fastapi import FastAPI, Path, Query
from pydantic import BaseModel, Field
app = FastAPI(title="Orders API", version="1.0.0")
class Order(BaseModel):
id: str = Field(..., description="Order identifier")
total: float = Field(..., description="Order total")
status: str = Field(..., description="Order status")
@app.get("/orders/{order_id}", response_model=Order)
async def get_order(
order_id: str = Path(..., description="Order ID")
):
"""Retrieve a single order by its identifier."""
return {"id": order_id, "total": 99.99, "status": "confirmed"}
In an Express application, the code-first approach usually involves JSDoc annotations parsed by swagger-jsdoc, or a dedicated tool that analyzes routes and validation schemas. The specification is assembled from annotations scattered across route files and combined with a base definition .
// Express route with JSDoc annotations for swagger-jsdoc
/**
* @openapi
* /orders/{id}:
* get:
* summary: Get an order by ID
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The order
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Order'
*/
app.get('/orders/:id', (req, res) => { /* ... */ });
The design-first approach inverts this. The specification is written in a text editor or Swagger Editor, validated, and then used to generate server stubs or client libraries. Swagger Codegen and OpenAPI Generator both accept a specification and produce code in dozens of languages . The specification is committed to the repository and reviewed like code; changes to the API contract are changes to the spec, not changes to annotations in scattered source files.
The practical trade-off is visibility versus control. Code-first keeps the specification close to the implementation, which makes it likely to stay current. Design-first makes the contract visible before implementation, which allows consumers to review it and catches mismatches earlier . Many teams use a hybrid: design the spec first, then implement against it, and use code-first generation to verify that the implementation and the spec have not diverged.
c. Rendering and CI Integration
A specification is only useful if it is rendered where consumers can read it. Swagger UI is the interactive renderer: it displays every endpoint, allows readers to expand parameters and schemas, and includes a “Try it out” console that sends live requests . Redoc is the read-only alternative: it produces a clean, three-panel reference site that many teams prefer for public documentation because it loads faster and emphasizes readability over interactivity .
<!-- Embedding Swagger UI in a page -->
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
<div id="swagger"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({ url: "/openapi.yaml", dom_id: "#swagger" });
</script>
<!-- Embedding Redoc in a page -->
<redoc spec-url="/openapi.yaml"></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
The choice between them is about audience. Swagger UI is for developers integrating against the API who need to send test requests. Redoc is for readers who need to understand the API structure without making calls . Both read the same specification, so switching later is a configuration change, not a rewrite.
CI integration is what prevents the specification from becoming stale. The pipeline should lint the spec on every commit, failing the build if the spec is invalid. The Redocly CLI provides both validation and static documentation generation, which means the docs can be built and deployed as part of the same pipeline that validates the spec .
# GitHub Actions step: validate spec and build docs
- name: Validate OpenAPI spec
run: npx @redocly/cli lint openapi.yaml
- name: Build static documentation
run: npx @redocly/cli build-docs openapi.yaml -o public/index.html
The validation step catches broken $ref references, missing required fields in schemas, and undocumented response codes. A specification that passes linting is not necessarily a good specificationโit can still omit authentication requirements or use vague descriptionsโbut it is structurally sound. The linting is the floor, not the ceiling.
For specifications that use $ref across multiple files, a bundling step produces a single self-contained document before rendering. The swagger-cli bundle command (or the Redocly CLI equivalent) resolves references and outputs one file that can be served or imported into other tools .
# Bundle a multi-file spec into a single document
npx @redocly/cli bundle openapi.yaml --output dist/openapi.bundle.yaml
Complete Example Session
# ============================================
# PART 1: SPECIFICATION HEADER
# ============================================
# openapi.yaml โ the contract for a task management API
openapi: 3.0.3
info:
title: Task Management API
version: 1.0.0
description: |
API for managing tasks and projects.
## Authentication
All endpoints require a Bearer token.
contact:
name: API Support
email: api@example.com
servers:
- url: https://api.example.com/v1
description: Production
- url: http://localhost:8000/v1
description: Development
# ============================================
# PART 2: SECURITY SCHEME
# ============================================
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
# ============================================
# PART 3: REUSABLE SCHEMA
# ============================================
components:
schemas:
Task:
type: object
required: [id, title, status]
properties:
id:
type: string
description: Unique task identifier
title:
type: string
description: Task title
status:
type: string
enum: [pending, in_progress, done]
description: Current task status
created_at:
type: string
format: date-time
# ============================================
# PART 4: ENDPOINT DEFINITION
# ============================================
paths:
/tasks:
get:
summary: List all tasks
operationId: listTasks
parameters:
- name: status
in: query
required: false
schema:
type: string
enum: [pending, in_progress, done]
responses:
'200':
description: A list of tasks
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Task'
'401':
description: Unauthorized
# ============================================
# PART 5: ENDPOINT WITH REQUEST BODY
# ============================================
/tasks/{id}:
get:
summary: Get a task by ID
operationId: getTask
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: The task
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
'404':
description: Task not found
patch:
summary: Update a task
operationId: updateTask
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title:
type: string
status:
type: string
enum: [pending, in_progress, done]
responses:
'200':
description: Task updated
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
# ============================================
# PART 6: CODE-FIRST GENERATION WITH FASTAPI
# ============================================
# main.py โ the same API implemented in FastAPI
from fastapi import FastAPI, Path
from pydantic import BaseModel, Field
app = FastAPI(title="Task Management API", version="1.0.0")
class Task(BaseModel):
id: str = Field(..., description="Unique task identifier")
title: str = Field(..., description="Task title")
status: str = Field(..., description="pending, in_progress, or done")
@app.get("/tasks/{task_id}", response_model=Task)
async def get_task(task_id: str = Path(..., description="Task ID")):
"""Retrieve a task by its identifier."""
return {"id": task_id, "title": "Example", "status": "pending"}
# ============================================
# PART 7: SWAGGER UI RENDERING
# ============================================
# Serve the spec with Swagger UI using the Docker image
# docker run -p 8080:8080 -e SWAGGER_JSON=/spec/openapi.yaml \
# -v $(pwd):/spec swaggerapi/swagger-ui
# Open http://localhost:8080 to interact with the API
# ============================================
# PART 8: REDOC STATIC BUILD
# ============================================
# Build a self-contained HTML file with Redoc
# npx @redocly/cli build-docs openapi.yaml -o docs/index.html
# The output can be hosted on any static file server
# ============================================
# PART 9: CI VALIDATION
# ============================================
# .github/workflows/openapi.yml
# - Lint the spec (fails on invalid references or schemas)
# - Build static docs
# - Deploy to GitHub Pages
# npx @redocly/cli lint openapi.yaml
# npx @redocly/cli build-docs openapi.yaml -o public/index.html
# ============================================
# PART 10: CLIENT GENERATION
# ============================================
# Generate a TypeScript client from the spec
# npx @openapitools/openapi-generator-cli generate \
# -i openapi.yaml -g typescript-fetch -o ./client
The ten parts trace the full documentation workflow: the specification structure, security schemes, reusable schemas, endpoint definitions, code-first generation, rendering with Swagger UI and Redoc, CI validation, and client generation. The specification is the artifact that every other step consumes.
Quick Reference
OpenAPI Document Sections
| Section | Purpose | Required |
|---|---|---|
openapi | Specification version | Yes |
info | Title, version, description | Yes |
servers | Base URLs | Recommended |
paths | Endpoint definitions | Yes |
components | Reusable schemas, security | Optional |
security | Global authentication requirement | Optional |
Specification Production Approaches
| Approach | Direction | Tool Examples |
|---|---|---|
| Design-first | Spec โ Code | Swagger Editor, OpenAPI Generator |
| Code-first | Code โ Spec | FastAPI, swagger-jsdoc, tsoa |
Rendering Tools
| Tool | Style | Best For |
|---|---|---|
| Swagger UI | Interactive, “try it out” | Developer integration |
| Redoc | Read-only, three-panel | Public reference |
| Stoplight Elements | Embeddable web component | Docs inside existing app |
CI Commands
| Command | Purpose |
|---|---|
redocly lint openapi.yaml | Validate spec structure |
redocly build-docs | Generate static HTML |
openapi-generator generate | Produce client SDK |
redocly bundle | Resolve $ref into single file |
Best Practices
โ Do This:
# Use $ref for reusable schemas
schema:
$ref: '#/components/schemas/Order' # โ
# Document every response code, including errors
responses:
'404':
description: Resource not found # โ
# Declare security requirements per endpoint or globally
security:
- bearerAuth: [] # โ
# Validate the spec in CI on every commit
npx @redocly/cli lint openapi.yaml # โ
# Let the framework generate the spec from types
@app.get("/users", response_model=UserListResponse) # โ
# Use the root version field to determine spec version
openapi: 3.0.3 # โ
โ Don’t Do This:
# Hand-write documentation instead of generating from spec
# Rendered docs and spec will drift # โ
# Omit error responses from endpoint definitions
responses:
'200': { description: OK } # โ
# Hardcode schemas inline instead of using components
schema:
type: object
properties: { ... } # โ
# Assume a file named swagger.yaml contains Swagger 2.0
# It may contain OpenAPI 3.x # โ
# Leave authentication undocumented
# Consumers cannot integrate without it # โ
# Skip CI validation and discover broken refs in production
# No lint step in pipeline # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Spec and implementation diverge | Code-first without validation | Generate spec from code or add contract tests |
Broken $ref links | Schema moved or renamed | Run redocly lint in CI |
| Undocumented error responses | Only happy path documented | Add 4xx and 5xx responses |
| Swagger UI shows “No operations” | Spec path wrong or invalid | Verify URL and lint spec |
File named swagger.yaml contains OpenAPI 3 | Naming is not indicative | Check root openapi field |
| Client generator produces wrong types | Schema definitions incomplete | Add required and format fields |
Real-World Examples
1. Minimal Spec Header
openapi: 3.0.3
info:
title: My API
version: 1.0.0
2. Endpoint with Path Parameter
/users/{id}:
get:
parameters:
- name: id
in: path
required: true
schema: { type: string }
3. Reusable Schema Reference
schema:
$ref: '#/components/schemas/User'
4. Security Scheme
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer }
5. Swagger UI Docker
docker run -p 8080:8080 -e SWAGGER_JSON=/spec/openapi.yaml \
-v $(pwd):/spec swaggerapi/swagger-ui
6. Redoc Static Build
npx @redocly/cli build-docs openapi.yaml -o index.html
7. CI Lint Step
- run: npx @redocly/cli lint openapi.yaml
8. FastAPI Auto-Generation
app = FastAPI(title="My API", version="1.0.0")
# Spec available at /openapi.json
9. Response Schema
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
10. Client Generation
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml -g typescript-fetch -o ./client
Visual
The Specification as Single Source of Truth
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ openapi.yaml โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Paths, schemas, security, responses โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ โ
โ โผ โผ โผ โผ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโ
โ โ Swagger UI โ โ Redoc โ โ Client Gen โ โ Mock โโ
โ โ Interactive โ โ Reference โ โ SDKs โ โ Server โโ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโ
โ โ
โ Every consumer reads the same file. โ
โ A change to the spec propagates to all of them. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Design-First vs Code-First
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DESIGN-FIRST CODE-FIRST โ
โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ Write spec โ โ Write code โ โ
โ โ (openapi.yaml) โ โ (annotations) โ โ
โ โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ โ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ Generate server โ โ Generate spec โ โ
โ โ stubs / client โ โ from code โ โ
โ โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ โ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ Implement โ โ Spec is derived โ โ
โ โ against spec โ โ from code โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Spec is authoritative. Code is authoritative. โ
โ Better for external APIs. Better for internal APIs. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
OpenAPI Document Structure
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ openapi: 3.0.3 โ Spec version โ
โ โ
โ info: โ
โ title: Orders API โ Human-readable metadata โ
โ version: 1.0.0 โ
โ โ
โ servers: โ
โ - url: https://api... โ Base URLs โ
โ โ
โ paths: โ
โ /orders: โ Endpoint definitions โ
โ get: โ
โ responses: โ
โ '200': โ
โ content: โ
โ application/json: โ
โ schema: โ
โ $ref: '#/components/schemas/Order' โ
โ โ
โ components: โ
โ schemas: โ Reusable definitions โ
โ Order: { ... } โ
โ securitySchemes: โ
โ bearerAuth: { ... } โ
โ โ
โ $ref connects paths to components. โ
โ The spec is one document with internal references. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
CI Validation Pipeline
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PUSH TO BRANCH โ
โ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ Lint spec โ redocly lint โ
โ โ โ Fails on broken refs, invalid schema โ
โ โโโโโโโโโโฌโโโโโโโโโ โ
โ โ PASS โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ Build docs โ redocly build-docs โ
โ โ โ Static HTML output โ
โ โโโโโโโโโโฌโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ Deploy docs โ GitHub Pages, Netlify, S3 โ
โ โ โ Published on merge โ
โ โโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Invalid spec blocks the merge. โ
โ Published docs always match the spec. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| OpenAPI | Specification format for REST APIs (YAML/JSON) |
| Swagger | Toolset implementing OpenAPI (UI, Editor, Codegen) |
| Root version field | openapi: 3.x.x or swagger: "2.0" |
| Paths object | Endpoint definitions by URL and method |
| Components object | Reusable schemas and security schemes |
$ref | Internal reference to components |
| Code-first tool | FastAPI (Python), swagger-jsdoc (Express) |
| Interactive renderer | Swagger UI |
| Read-only renderer | Redoc |
| CI validation | redocly lint openapi.yaml |
Key takeaways:
- OpenAPI is the standard; Swagger is the toolset. The specification was donated to the OpenAPI Initiative in 2015 and renamed. Swagger refers to the tools that consume itโSwagger UI, Swagger Editor, Swagger Codegen . A “Swagger file” is an OpenAPI document; a “Swagger UI” renders it.
- The specification is the contract. Documentation, client libraries, and mock servers are all derived from it. Hand-editing rendered docs instead of the spec guarantees drift. The CI pipeline should validate the spec on every commit .
- Code-first and design-first both produce valid specs. FastAPI generates the spec from type hints and Pydantic models automatically. Design-first writes the spec and generates server stubs from it. The choice depends on whether the API is designed for external consumers or documented after implementation .
$refprevents duplication. Schemas defined once incomponentsare referenced from every endpoint. A change to a schema propagates everywhere it is used, which is exactly what a contract should do.- Swagger UI and Redoc serve different audiences. Swagger UI is interactive and includes a “try it out” console for developers integrating against the API. Redoc is read-only and optimized for readability. Both read the same spec, so the choice is about presentation, not capability .
- Validation must be automated. A syntactically valid spec can still have broken
$reflinks, missing required fields, or undocumented response codes. A lint step in CI catches these before they reach consumers.
Remember: The OpenAPI document is not documentation; it is the source from which documentation is generated. The distinction matters because it determines where changes are made. When an endpoint changes, the spec changes, and every consumerโSwagger UI, Redoc, client SDKs, contract testsโupdates automatically. When the spec is treated as a secondary artifact, it drifts, and the documentation it produces becomes misleading. The tools are interchangeable; the specification is not. Whether the team uses Swagger UI or Redoc, FastAPI or design-first YAML, the contract lives in one place and everything else derives from it. That is the discipline the objective requires.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!