A RESTful API for managing a mechanic shop built with Flask using the Application Factory Pattern. Supports managing customers, mechanics, service tickets, and inventory parts with many-to-many relationships, JWT authentication, rate limiting, caching, pagination, Swagger documentation, and unit testing.
- Python + Flask — web framework
- Flask-SQLAlchemy — ORM for database models
- Flask-Marshmallow + marshmallow-sqlalchemy — serialization and validation
- MySQL — relational database
- mysql-connector-python — MySQL driver
- Flask-Limiter — rate limiting
- Flask-Caching — response caching
- python-jose — JWT token encoding and decoding
- flask-swagger — Swagger spec support
- flask-swagger-ui — Swagger UI interface
- gunicorn — Production WSGI server
- psycopg2-binary — PostgreSQL driver
- GitHub Actions — CI/CD pipeline
- Render — Cloud deployment platform
The API is deployed on Render and publicly accessible.
https://mechanic-shop-api-deqw.onrender.com
https://mechanic-shop-api-deqw.onrender.com/api/docs
https://mechanic-shop-api-deqw.onrender.com/api/swagger.json
The API is deployed on Render using Gunicorn as the production WSGI server. PostgreSQL is hosted on Render and linked automatically via the DATABASE_URI environment variable. All sensitive configuration is loaded from environment variables — no secrets are stored in the repository.
| Variable | Description |
|---|---|
DATABASE_URI |
PostgreSQL database connection string |
SECRET_KEY |
JWT and Flask secret key |
Copy .env.example to .env and fill in your values for local development. The .env file is listed in .gitignore and must never be committed.
This project uses GitHub Actions for Continuous Integration and Continuous Deployment.
Pipeline workflow (triggered on every push to main):
- Install all dependencies from
requirements.txt. - Run the full automated test suite (
python -m unittest discover tests). - Deploy automatically to Render if all tests pass.
Deployment is blocked if any test fails — the deploy job depends on needs: test.
The workflow file is at .github/workflows/main.yaml. Two GitHub Secrets are required: RENDER_API_KEY and SERVICE_ID.
| Feature | Details |
|---|---|
| JWT Authentication | Token-based auth on protected routes |
| Swagger Documentation | Interactive UI at /api/docs |
| Inventory Management | Full CRUD for parts |
| Pagination | ?page and per_page on list endpoints |
| Rate Limiting | 5 req/min on customer list; 200/day global |
| Caching | 60-second in-memory cache on mechanic list |
| Automated Testing | 69 tests across all route modules |
| PostgreSQL Database | Hosted on Render in production |
| Render Deployment | Live at mechanic-shop-api-deqw.onrender.com |
| GitHub Actions CI/CD | Auto-deploy on push to main after tests pass |
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # Mac/Linuxpip install -r requirements.txtLog in to MySQL and run:
CREATE DATABASE mechanic_shop_db;Open app/__init__.py and update the SQLALCHEMY_DATABASE_URI with your MySQL credentials.
python app.pyTables are created automatically on first run. The server starts at http://127.0.0.1:5000.
Interactive API documentation is available at:
http://127.0.0.1:5000/api/docs
The raw OpenAPI JSON spec is available at:
http://127.0.0.1:5000/api/swagger.json
- Start the app with
python app.py - Open your browser and go to
http://127.0.0.1:5000/api/docs - Expand any endpoint to see its description, parameters, and example request/response
- For token-protected routes, first use
POST /customers/loginto get a JWT token, then click Authorize (lock icon) and enterBearer <your_token> - Click Try it out on any endpoint to send live requests
- The spec is defined in
app/swagger.pyas a Python dictionary - Two blueprints are registered in
app/__init__.py: one serving the JSON spec and one serving the UI - The spec follows OpenAPI 2.0 (Swagger 2.0) format
From the project root directory, run:
python -m unittest discover testsTo run a specific test file:
python -m unittest tests.test_customers
python -m unittest tests.test_mechanics
python -m unittest tests.test_service_tickets
python -m unittest tests.test_inventoryTo run a specific test case:
python -m unittest tests.test_customers.TestCustomers.test_create_customer_success- Tests use SQLite in-memory database — the production MySQL database is never touched
- Rate limiting is disabled during tests
- Each test creates a fresh database and tears it down after, so tests are fully isolated
- The shared test configuration lives in
tests/base.py
Each test file covers both positive (success) and negative (error) cases:
| File | Routes Tested |
|---|---|
tests/test_customers.py |
POST, GET, PUT, DELETE /customers/, login, my-tickets |
tests/test_mechanics.py |
POST, GET, PUT, DELETE /mechanics/, most-tickets |
tests/test_service_tickets.py |
POST, GET /service-tickets/, assign, remove, edit, add-part |
tests/test_inventory.py |
POST, GET, PUT, DELETE /inventory/ |
Current test coverage:
- Customer routes
- Mechanic routes
- Service Ticket routes
- Inventory routes
- Authentication routes
- Positive and negative test cases for every module
Total automated tests: 69
GET /customers/is limited to 5 requests per minute.- A default limit of 200 per day / 50 per hour applies to all routes.
- Returns HTTP 429 with a plain-text message when exceeded.
GET /mechanics/is cached for 60 seconds using SimpleCache (in-memory).- Subsequent requests within 60 seconds return the cached response without hitting the database.
POST /customers/loginreturns a signed JWT token (valid for 1 hour).- Protected routes require the header:
Authorization: Bearer <token> - Decoded payload contains
customer_idwhich is injected into the route function. - Error responses: 401 with a JSON
{"error": "..."}message for missing, invalid, or expired tokens.
GET /customers/supports?page=1&per_page=5query parameters.- Response includes
customers,page,per_page,total, andpages.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /customers/ |
No | Create a new customer |
| GET | /customers/ |
No | Get all customers (paginated, rate limited) |
| GET | /customers/<id> |
No | Get customer by ID |
| PUT | /customers/<id> |
No | Update customer by ID |
| DELETE | /customers/<id> |
No | Delete customer by ID |
| POST | /customers/login |
No | Login and receive JWT token |
| GET | /customers/my-tickets |
JWT | Get service tickets for logged-in customer |
POST /customers/ body:
{
"name": "John Smith",
"email": "john@example.com",
"phone": "555-1234",
"address": "123 Main St",
"password": "mypassword123"
}POST /customers/login body:
{
"email": "john@example.com",
"password": "mypassword123"
}GET /customers/ with pagination:
GET /customers/?page=1&per_page=5
GET /customers/my-tickets — Authorization header:
Authorization: Bearer <your_jwt_token>
| Method | Endpoint | Description |
|---|---|---|
| POST | /mechanics/ |
Create a new mechanic |
| GET | /mechanics/ |
Get all mechanics (cached 60s) |
| GET | /mechanics/<id> |
Get mechanic by ID |
| PUT | /mechanics/<id> |
Update mechanic by ID |
| DELETE | /mechanics/<id> |
Delete mechanic by ID |
| GET | /mechanics/most-tickets |
Mechanics ranked by ticket count |
POST /mechanics/ body:
{
"name": "Jane Doe",
"email": "jane@shop.com",
"phone": "555-5678",
"address": "456 Garage Ave",
"salary": 55000.00
}| Method | Endpoint | Description |
|---|---|---|
| POST | /service-tickets/ |
Create a new service ticket |
| GET | /service-tickets/ |
Get all service tickets |
| PUT | /service-tickets/<ticket_id>/assign-mechanic/<mechanic_id> |
Assign mechanic to ticket |
| PUT | /service-tickets/<ticket_id>/remove-mechanic/<mechanic_id> |
Remove mechanic from ticket |
| PUT | /service-tickets/<ticket_id>/edit |
Bulk add/remove mechanics |
| PUT | /service-tickets/<ticket_id>/add-part/<inventory_id> |
Add inventory part to ticket |
POST /service-tickets/ body:
{
"customer_id": 1,
"vin": "1HGBH41JXMN109186",
"service_description": "Oil change and tire rotation",
"status": "open"
}PUT /service-tickets//edit body:
{
"add_ids": [1, 2],
"remove_ids": [3]
}| Method | Endpoint | Description |
|---|---|---|
| POST | /inventory/ |
Create a new inventory part |
| GET | /inventory/ |
Get all inventory parts |
| GET | /inventory/<id> |
Get inventory part by ID |
| PUT | /inventory/<id> |
Update inventory part by ID |
| DELETE | /inventory/<id> |
Delete inventory part by ID |
POST /inventory/ body:
{
"name": "Oil Filter",
"price": 12.99
}Test in this order to avoid dependency errors:
- POST /mechanics/ — Create 2–3 mechanics
- POST /customers/ — Create a customer (include
password) - POST /customers/login — Login to get JWT token
- POST /service-tickets/ — Create a service ticket using
customer_id - POST /inventory/ — Create inventory parts
- PUT /service-tickets//assign-mechanic/ — Assign a mechanic
- PUT /service-tickets//edit — Bulk edit mechanic assignments
- PUT /service-tickets//add-part/ — Add inventory part to ticket
- GET /customers/my-tickets — Use the JWT token in Authorization header
- GET /customers/ — Test pagination with
?page=1&per_page=5 - GET /mechanics/most-tickets — See ranking by ticket count
- GET /mechanics/ — Make the same request 3× within 60s to confirm caching
- One Customer can have many Service Tickets (One-to-Many)
- Many-to-Many via
service_ticket_mechanicsassociation table
- Many-to-Many via
service_ticket_inventoryassociation table
Customer — id, name, email, phone, address, password
Mechanic — id, name, email, phone, address, salary
ServiceTicket — id, customer_id, vin, service_description, status, created_at
Inventory — id, name, price
The application uses environment variables for sensitive configuration in production. Copy .env.example to .env and fill in your values:
cp .env.example .env| Variable | Description |
|---|---|
DATABASE_URI |
Full PostgreSQL connection string (provided by Render) |
SECRET_KEY |
Secret key used to sign JWT tokens |
.env is listed in .gitignore and must never be committed to source control.
https://mechanic-shop-api-deqw.onrender.com
https://mechanic-shop-api-deqw.onrender.com/api/docs
https://mechanic-shop-api-deqw.onrender.com/api/swagger.json
| Variable | Description |
|---|---|
DATABASE_URI |
PostgreSQL connection string (provided by Render when a database is linked) |
SECRET_KEY |
Secret key used to sign JWT tokens |
- Push this repository to GitHub.
- Go to https://render.com and sign in.
- Click New → Web Service and connect your GitHub repository.
- Configure the service:
- Name:
mechanic-shop-api-deqw - Environment: Python
- Build Command:
pip install -r requirements.txt - Start Command:
gunicorn flask_app:app
- Name:
- Under Environment Variables, add
DATABASE_URIandSECRET_KEY. - Click Create Web Service. Render will build and deploy automatically.
The workflow file is at .github/workflows/main.yaml.
| Job | Trigger | What it does |
|---|---|---|
build |
Push to main |
Checks out code, sets up Python 3.11, installs dependencies |
test |
After build |
Runs python -m unittest discover tests against an SQLite in-memory DB |
deploy |
After test passes |
Calls the Render Deploy API to trigger a new deployment |
Go to your GitHub repository → Settings → Secrets and variables → Actions → New repository secret and add:
| Secret | Where to find it |
|---|---|
RENDER_API_KEY |
Render dashboard → Account → API Keys |
SERVICE_ID |
Render dashboard → your service → Settings → Service ID (starts with srv-) |
- Push any commit to the
mainbranch. - Go to your GitHub repository → Actions tab.
- You should see a workflow run with three jobs:
build → test → deploy. - If all three pass (green), a new deployment is triggered on Render automatically.
- Check the Render dashboard → your service → Events tab to confirm the deploy was received..
Abdelrahman Yousef