Chapter 4.6: Creating a Simple Interface for the API
Study time: 1 hour 15 minutes
1. Final Assembly: Launching the "Mission Control Center"
We have studied all the systems separately: configured the "antenna" (Fetch
), learned to send "commands" (GET
, POST
, DELETE
), and developed "emergency protocols" (error handling).
It's time to bring all the components together and launch our Mission Control — a full-fledged, interactive interface for managing the space fleet.
Our goal:
- Create a single, clean, and understandable interface.
- Implement the full CRUD cycle: creating, displaying, updating, and deleting ships.
- Add visual feedback for the user (loading, success, error).
💡 Space Analogy:
We are moving from individual test consoles to the main screen of Mission Control. It must have all the necessary buttons and indicators so that a single operator can manage the entire fleet without switching between dozens of different systems.
2. Interface Design: The "Dashboard"
We will need more structured HTML. We will use "cards" to display the ships and a modal window for editing them.
Updating index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MCC v2.0 - Fleet Management</title>
<link rel="stylesheet" href="style.css"> <!-- Include styles -->
</head>
<body>
<header>
<h1>Space Fleet Control Panel</h1>
</header>
<main>
<section id="fleet-controls">
<button id="load-fleet-btn">Refresh Fleet List</button>
<button id="show-create-form-btn">Launch New Craft</button>
</section>
<section id="fleet-display">
<h2>Current Fleet Composition</h2>
<div id="fleet-list" class="cards-container">
<!-- Ship cards will be here -->
</div>
</section>
</main>
<!-- Modal for creating/editing (initially hidden) -->
<div id="modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<h2 id="modal-title">Launch New Craft</h2>
<form id="ship-form">
<input type="hidden" id="ship-id">
<input type="text" id="ship-name" placeholder="Name" required>
<input type="text" id="ship-type" placeholder="Type" required>
<input type="number" id="ship-year" placeholder="Launch Year" required>
<input type="text" id="ship-status" placeholder="Status" required>
<div class="modal-actions">
<button type="submit" id="save-btn">Save</button>
<button type="button" id="cancel-btn">Cancel</button>
</div>
</form>
<div id="notification-area"></div>
</div>
</div>
<script src="api.js"></script>
<script src="app.js"></script>
</body>
</html>
3. Adding a "Cosmic" Design: style.css
Create a style.css
file to make our Mission Control look decent.
/* style.css */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #1a1a2e;
color: #e0e0e0;
margin: 0;
padding: 20px;
}
header { text-align: center; margin-bottom: 20px; }
button {
background-color: #4a4e69;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover { background-color: #6a6e94; }
.cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.card {
background-color: #2a2a4e;
border: 1px solid #4a4e69;
border-radius: 8px;
padding: 15px;
}
.card h3 { margin-top: 0; color: #9394a5; }
.card-actions { margin-top: 15px; }
/* Styles for the modal window */
.modal-overlay {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-color: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: #1a1a2e;
padding: 20px;
border-radius: 8px;
border: 1px solid #4a4e69;
width: 90%;
max-width: 500px;
}
#ship-form input {
width: calc(100% - 20px);
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
border: 1px solid #4a4e69;
background-color: #2a2a4e;
color: white;
}
.modal-actions { text-align: right; }
4. Complete Logic Overhaul: app.js
Now we will write the final version of app.js
, combining all our knowledge.
// app.js
// --- DOM Elements ---
const loadFleetBtn = document.getElementById('load-fleet-btn');
const fleetListContainer = document.getElementById('fleet-list');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modal-title');
const shipForm = document.getElementById('ship-form');
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('cancel-btn');
const showCreateFormBtn = document.getElementById('show-create-form-btn');
const notificationArea = document.getElementById('notification-area');
// --- UI Functions ---
function showNotification(message, isError = false) {
notificationArea.textContent = message;
notificationArea.style.color = isError ? '#ff6b6b' : '#6bff6b';
}
function openModalForCreate() {
shipForm.reset();
document.getElementById('ship-id').value = '';
modalTitle.textContent = 'Launch New Craft';
modal.style.display = 'flex';
}
function openModalForEdit(ship) {
shipForm.reset();
document.getElementById('ship-id').value = ship.id;
document.getElementById('ship-name').value = ship.name;
document.getElementById('ship-type').value = ship.type;
document.getElementById('ship-year').value = ship.launch_year;
document.getElementById('ship-status').value = ship.status;
modalTitle.textContent = `Editing: ${ship.name}`;
modal.style.display = 'flex';
}
function closeModal() {
modal.style.display = 'none';
notificationArea.textContent = '';
}
function createShipCard(ship) {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<h3>${ship.name} (ID: ${ship.id})</h3>
<p>Type: ${ship.type}</p>
<p>Launch Year: ${ship.launch_year}</p>
<p>Status: ${ship.status}</p>
<div class="card-actions">
<button class="edit-btn" data-ship-id="${ship.id}">Edit</button>
<button class="delete-btn" data-ship-id="${ship.id}">Decommission</button>
</div>
`;
return card;
}
// --- API and Display Logic ---
async function fetchAndDisplayFleet() {
try {
fleetListContainer.innerHTML = '<p>Loading telemetry...</p>';
const ships = await apiRequest('/spaceships');
fleetListContainer.innerHTML = '';
if (ships.length === 0) {
fleetListContainer.innerHTML = '<p>No crafts in the registry.</p>';
return;
}
ships.forEach(ship => {
const card = createShipCard(ship);
fleetListContainer.appendChild(card);
});
} catch (error) {
fleetListContainer.innerHTML = `<p style="color: #ff6b6b;">Error loading fleet: ${error.message}</p>`;
}
}
async function handleSaveShip(event) {
event.preventDefault();
const shipId = document.getElementById('ship-id').value;
const shipData = {
name: document.getElementById('ship-name').value,
type: document.getElementById('ship-type').value,
launch_year: parseInt(document.getElementById('ship-year').value),
status: document.getElementById('ship-status').value
};
try {
let response;
if (shipId) {
// Update (PUT)
response = await apiRequest(`/spaceships/${shipId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(shipData)
});
showNotification(`Craft "${response.name}" successfully updated!`);
} else {
// Create (POST)
response = await apiRequest('/spaceships', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(shipData)
});
showNotification(`Craft "${response.name}" successfully launched! ID: ${response.id}`);
}
setTimeout(() => {
closeModal();
fetchAndDisplayFleet();
}, 1500);
} catch (error) {
showNotification(error.message, true);
}
}
async function handleDeleteShip(shipId) {
if (!confirm(`Are you sure you want to decommission the craft with ID ${shipId}?`)) return;
try {
await apiRequest(`/spaceships/${shipId}`, { method: 'DELETE' });
alert('Craft successfully decommissioned.');
fetchAndDisplayFleet();
} catch (error) {
alert(`Error during decommissioning: ${error.message}`);
}
}
// --- Event Handlers ---
document.addEventListener('DOMContentLoaded', fetchAndDisplayFleet);
loadFleetBtn.addEventListener('click', fetchAndDisplayFleet);
showCreateFormBtn.addEventListener('click', openModalForCreate);
cancelBtn.addEventListener('click', closeModal);
shipForm.addEventListener('submit', handleSaveShip);
fleetListContainer.addEventListener('click', async (event) => {
const target = event.target;
if (target.classList.contains('delete-btn')) {
handleDeleteShip(target.dataset.shipId);
}
if (target.classList.contains('edit-btn')) {
try {
const ship = await apiRequest(`/spaceships/${target.dataset.shipId}`);
openModalForEdit(ship);
} catch (error) {
alert(`Failed to load data for editing: ${error.message}`);
}
}
});
5. Final Tests
- Start the FastAPI server:
uvicorn main:app --reload
- Open
index.html
in your browser (via Live Server). - Check the full cycle:
- The list of ships should load automatically.
- Click "Launch New Craft", fill out the form, and save. Make sure the new ship appears in the list.
- Click "Edit" on any ship, change the data, and save. Make sure the information is updated.
- Click "Decommission" on any ship, confirm the action. Make sure it disappears from the list.
- Check all error scenarios (invalid data, server shutdown).
Quiz to Reinforce
🚀 Chapter Summary
You have successfully completed the construction and launch of your "Mission Control Center".
- 🖥️ You have created a structured and styled HTML/CSS interface.
- ⚙️ You have written clean, modular JavaScript code that implements the full CRUD cycle.
- 🛰️ Your frontend can now fully manage the backend created with FastAPI.
Congratulations on successfully completing Chapter 4! You have come all the way from sending a simple fetch
request to creating a full-fledged web application that interacts with your own API.