parking-app
Fullstack parking-reservation platform with a Django REST backend, Vue 3 frontend, Docker orchestration, and CI/CD pipeline. The project covers hiring-relevant engineering topics: documented API (drf-spectacular), token auth, permissions, PostGIS geospatial model, TypeScript protected routing, backend tests, and automated deployment.
🎯 Context and goals
- Deliver an application where authenticated users can browse parking lots, filter available spots, create reservations, and manage their own reservations.
- Establish a delivery architecture usable in development and production through Docker Compose, Nginx TLS reverse proxy, quality checks, and CI deployment.
- Enforce maintainability through explicit domain modeling, paginated/filterable APIs, typed frontend error handling, and automated backend tests.
🛠️ Deliverables
🧩 Design
- The backend stack is designed around Django + DRF + drf-spectacular + PostGIS + CORS/auth, with clear runtime/dev dependency separation. Source: parking-app/backend/requirements.txt
Django>=4.2
wheel
setuptools
djangorestframework
drf-spectacular
gunicorn
psycopg2-binary
django-allauth
dj-rest-auth
requests
jwt
django-cors-headers
djangorestframework-gis
python-decouple
- The frontend is structured with Vue 3 + TypeScript + Vue Router + Tailwind + Headless UI, with lint/format integrated into scripts. Source: parking-app/frontend/package.json
"scripts": {
"dev": "vite --host",
"build": "vite build",
"format:check": "prettier --config .prettierrc --ignore-path .prettierignore --check 'src/**/*.{vue,ts,js,json,css,scss,html}'",
"lint": "eslint . --ext .ts,.tsx"
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"vue": "^3.4.0",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.3",
"@vitejs/plugin-vue": "^5.2.3",
"eslint": "^9.24.0",
"prettier": "^3.3.3",
"tailwindcss": "^4.1.3",
"typescript": "^5.0.0",
"vite": "^5.0.0"
}
- Backend configuration formalizes architecture decisions: PostGIS database engine, DRF token auth, OpenAPI/Swagger docs, and environment-based CORS policy. Source: parking-app/backend/config/settings.py
INSTALLED_APPS = [
"corsheaders",
"django.contrib.sites",
"allauth",
"allauth.account",
"allauth.socialaccount",
"dj_rest_auth",
"dj_rest_auth.registration",
"parking",
"rest_framework",
"rest_framework.authtoken",
"drf_spectacular",
"django.contrib.gis",
]
DATABASES = {
"default": {
"ENGINE": "django.contrib.gis.db.backends.postgis",
"NAME": os.environ.get("POSTGRES_DB", "parking_db"),
"HOST": os.environ.get("DB_HOST", "db"),
"PORT": os.environ.get("DB_PORT", "5432"),
}
}
💻 Development
- Backend : The domain model implements a custom user entity, reservation/parking entities, and geospatial logic (point-in-polygon) to automatically attach lots to a city. Source: parking-app/backend/parking/models.py
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError("Email obligatoire")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
USERNAME_FIELD = "email"
objects = CustomUserManager()
class ParkingLot(models.Model):
name = models.CharField(max_length=100)
position = gis_models.PointField(srid=4326, null=True)
city = models.ForeignKey("City", null=True, blank=True, on_delete=models.SET_NULL, related_name="parking_lots")
def save(self, *args, **kwargs):
if self.position:
matching_city = City.objects.filter(geom__contains=self.position).first()
if matching_city:
self.city = matching_city
super().save(*args, **kwargs)
Serialization uses an expand pattern to return relation details only when requested, avoiding over-fetching by default.
Source: parking-app/backend/parking/serializers.py
class ParkingSpotSerializer(serializers.ModelSerializer):
parking_lot = serializers.SerializerMethodField()
class Meta:
model = ParkingSpot
fields = ["id", "number", "available", "price", "parking_lot"]
def get_parking_lot(self, obj):
request = self.context.get("request")
if is_expanded(request, "lot"):
return ParkingLotSerializer(obj.parking_lot, context=self.context).data
return obj.parking_lot.id
class ReservationSerializer(serializers.ModelSerializer):
parking_spot_detail = serializers.SerializerMethodField()
def get_parking_spot_detail(self, obj):
request = self.context.get("request")
if is_expanded(request, "spot"):
return ParkingSpotSerializer(obj.parking_spot, context=self.context).data
return None
ViewSets implement pagination, query-param filtering, and business-state transitions (reservation makes spot unavailable, deletion restores availability). Source: parking-app/backend/parking/views.py
class DefaultPagination(PageNumberPagination):
page_size = 5
page_size_query_param = "page_size"
max_page_size = 50
class ParkingSpotViewSet(viewsets.ModelViewSet):
serializer_class = ParkingSpotSerializer
pagination_class = DefaultPagination
def get_queryset(self):
queryset = ParkingSpot.objects.all()
parking_lot_id = self.request.query_params.get("parking_lot")
available = self.request.query_params.get("available")
if parking_lot_id:
queryset = queryset.filter(parking_lot=parking_lot_id)
if available == "true":
queryset = queryset.filter(available=True)
elif available == "false":
queryset = queryset.filter(available=False)
return queryset
class ReservationViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
reservation = serializer.save(user=self.request.user)
reservation.parking_spot.available = False
reservation.parking_spot.save()
def perform_destroy(self, instance):
spot = instance.parking_spot
instance.delete()
spot.available = True
spot.save()
Critical business behavior is validated with API tests, including spot availability transitions after reservation. Source: parking-app/backend/parking/tests/test_views.py
@pytest.mark.django_db
def test_reservation_sets_spot_unavailable():
client = APIClient()
user = User.objects.create_user(email="test@example.com", password="pass123")
client.force_authenticate(user=user)
lot = ParkingLot.objects.create(name="Lot A", position=Point(4, 4))
spot = ParkingSpot.objects.create(parking_lot=lot, number=1, available=True, price=10)
response = client.post(
reverse("reservations-list"),
{"parking_spot": str(spot.id)},
format="json",
)
assert response.status_code == 201
reservation = Reservation.objects.first()
assert reservation is not None
assert reservation.parking_spot.id == spot.id
spot.refresh_from_db()
assert spot.available is False
- Frontend :
Routing uses authentication guards (
requiresAuth/guestOnly) to protect private pages and avoid invalid navigation states. Source: parking-app/frontend/src/router/index.ts
const routes = [
{
path: '/',
component: DefaultLayout,
children: [
{ path: '', redirect: '/dashboard' },
{ path: 'login', component: LoginPage, meta: { guestOnly: true } },
{ path: 'register', component: RegisterPage, meta: { guestOnly: true } },
{ path: 'dashboard', component: DashboardPage, meta: { requiresAuth: true } },
{ path: 'reservation', component: ReservationForm, meta: { requiresAuth: true } },
],
},
];
router.beforeEach((to, from, next) => {
const isAuthenticated = AuthService.isAuthenticated();
if (to.meta.requiresAuth && !isAuthenticated) {
return next({ name: 'Login' });
}
if (to.meta.guestOnly && isAuthenticated) {
return next({ name: 'Dashboard' });
}
return next();
});
HTTP access is centralized in a typed generic client handling auth token injection, backend validation payloads, and network failures. Source: parking-app/frontend/src/services/api.ts
export class ApiError extends Error {
status: number;
details?: ErrorDetails;
constructor(message: string, status: number, details?: ErrorDetails) {
super(message);
this.name = 'ApiError';
this.status = status;
this.details = details;
}
}
export async function apiFetch<T>(
endpoint: string,
options: RequestInit = {},
includeAuthHeader: boolean = true
): Promise<T> {
const token = includeAuthHeader ? AuthService.getToken() : null;
const headers = {
'Content-Type': 'application/json',
...(token ? { Authorization: `Token ${token}` } : {}),
...(options.headers || {}),
};
const url = endpoint.startsWith('http') ? endpoint : `${API_BASE_URL}${endpoint}`;
const res = await fetch(url, { ...options, headers });
if (!res.ok) {
const errorBody = await res.json();
throw new ApiError(errorBody.detail || `HTTP ${res.status}`, res.status, errorBody);
}
return (await res.json()) as T;
}
The reservation UI implements Headless UI comboboxes, paginated loading, dynamic lot/spot filtering, and structured server-error rendering. Source: parking-app/frontend/src/views/ReservationForm.vue
async function loadLots(url: string = '/parkinglots/?page_size=10') {
try {
const response = await apiFetch<PaginatedResponse<ParkingLot>>(url);
lots.value.push(...response.results);
lotsPagination.value.next = response.next;
} catch (e) {
errors.value = { global: ['Erreur lors du chargement des parkings.'] };
}
}
watch(selectedLotId, async newLotId => {
if (newLotId) {
availableSpots.value = [];
const initialUrl = `/parkingspots/?parking_lot=${newLotId}&available=true&page_size=10`;
await loadSpots(initialUrl);
}
});
async function submitReservation() {
const user = AuthService.getUser();
try {
await apiFetch('/reservations/', {
method: 'POST',
body: JSON.stringify({
parking_spot: selectedSpotId.value,
user: user.pk,
}),
});
success.value = true;
errors.value = {};
} catch (err: any) {
if (err instanceof ApiError && err.details) {
errors.value = err.details;
}
}
}
🏗️ DevOps & Quality
- The application is orchestrated with Docker Compose (backend, frontend, PostGIS), then extended in production with Nginx TLS reverse proxy. Source: parking-app/docker-compose.yml
services:
backend:
build:
context: ./backend
container_name: parking_backend
depends_on:
- db
frontend:
build:
context: ./frontend
container_name: parking_frontend
depends_on:
- backend
db:
image: postgis/postgis:15-3.3
container_name: parking_db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
- Production deployment is automated with GitHub Actions + SSH, including
git pull, compose rebuild, Django migration, and service health verification. Source: parking-app/.github/workflows/deploy.yml
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd $BASE_DIR_APP/parking_app
git pull origin master
docker compose -f docker-compose.yml -f docker-compose.prod.yml down
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backend python manage.py migrate
- name: ✅ Vérification des containers Docker
uses: appleboy/ssh-action@v1.0.0
with:
script: |
RUNNING=$(docker compose -f docker-compose.yml -f docker-compose.prod.yml ps --services --filter "status=running" | wc -l)
TOTAL=$( docker compose -f docker-compose.yml -f docker-compose.prod.yml config --services | wc -l)
if [ "$RUNNING" -ne "$TOTAL" ]; then
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
exit 1
fi
📈 Results
- Based on the repository Git history, the delivered scope spans 2025-04-11 -> 2025-04-14, with 15 commits across 2 contributors. The implementation provides a full functional chain: token authentication, paginated/filterable CRUD APIs, PostGIS geospatial model, protected SPA navigation, and reservation workflows with coherent availability transitions. Delivery readiness is reinforced through containerization, CI/CD workflow automation, and backend tests targeting critical behavior. The technical benefit is a deployable and maintainable fullstack baseline aligned with production-oriented engineering expectations.
🔧 Technical environment
- Backend: Django 5, Django REST Framework, dj-rest-auth, django-allauth, drf-spectacular (OpenAPI/Swagger), djangorestframework-gis, Gunicorn.
- Frontend: Vue 3, strict TypeScript, Vue Router, Headless UI, Vite, Tailwind CSS.
- Database and geo: PostgreSQL/PostGIS, GeoDjango
PointFieldandPolygonField. - Quality: Pytest/pytest-django, Black, Flake8, ESLint, Prettier.
- Infrastructure: Docker, Docker Compose, Nginx TLS reverse proxy.
- CI/CD: GitHub Actions (Django tests, lint, SSH deployment).