"This Star Wars-themed API project is a full-stack backend exercise crafted to reinforce key skills in RESTful API development. Using Express.js and MongoDB with Mongoose, it allows developers to practice creating and testing CRUD operations, implementing validation, error handling, and working with dynamic routes. It's an engaging, hands-on way to apply theoretical knowledge in a themed and memorable context."
- Star Wars Character Database CRUD API
Create a REST API that manages a database of Star Wars characters. Users should be able to create, read, update, and delete character information through various endpoints.
project-root/
├── backend/
│ ├── config/
│ │ └── db.js
│ ├── controller/
│ │ └── characterController.js
│ ├── libs/
│ │ ├── seeds.js
│ ├── middleware/
│ │ ├── authMiddleware.js
│ │ └── requireAdmin.js
│ ├── models/
│ │ ├── characterModel.js
│ │ └── userModel.js
│ ├── routes/
│ │ ├── characterRoutes.js
│ │ ├── publicRoutes.js
│ │ └── userProfile.js
│ ├── app.js
│ └── package.json
├── frontend/
│ ├── public/
│ ├── src/
│ │ ├── components/
│ │ │ ├── buttons/
│ │ │ │ ├── BtnNeonGradient.jsx
│ │ │ │ ├── Button.jsx
│ │ │ │ ├── ButtonGradient.jsx
│ │ │ │ ├── ButtonSvg.jsx
│ │ │ │ ├── SpaceBtn.jsx
│ │ │ │ └── SpaceBtnSvg.jsx
│ │ │ ├── form/
│ │ │ │ ├── ArrayInput.jsx
│ │ │ │ ├── CheckboxInput.jsx
│ │ │ │ ├── NumberInput.jsx
│ │ │ │ └── TextInput.jsx
│ │ │ ├── hooks/
│ │ │ │ └── UserProfileFetches.js
│ │ │ ├── reg-auth/
│ │ │ │ ├── LoginForm.jsx
│ │ │ │ └── RegisterForm.jsx
│ │ │ ├── spaceAtmos/
│ │ │ │ ├── NebulaCanvas.jsx
│ │ │ │ ├── nebulaCloud.js
│ │ │ │ └── star.js
│ │ │ └── utils/
│ │ │ │ ├── api.js
│ │ │ │ ├── auth.js
│ │ │ │ └── debounce.js
│ │ │ └── views/
│ │ │ │ ├── InfoPage.jsx
│ │ │ │ └── UserProfile.jsx
│ │ │ ├── CharacterDetail.jsx
│ │ │ ├── Characters.jsx
│ │ │ └── CharacterForm.jsx
│ │ │ └── ViewRouter.jsx
│ │ ├── context/
│ │ │ ├── AppContext.jsx
│ │ ├── App.jsx
│ │ ├── App.css
│ │ └── index.css
│ ├── main.jsx
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.js
│ └── tailwind.config.js
├── .gitignore
├── README.md- Syntax: Cleaner and more readable, especially for complex logic.
- Error Handling: Use
try/catchblocks to handle errors. - Sequential Execution: Makes asynchronous code look synchronous, which is easier to follow.
- Example:
**Example
const handleSubmit = async (e) => { e.preventDefault(); try { const response = await fetch("http://localhost:5000/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); const data = await response.json(); if (response.ok) { console.log("Token received:", data.token); localStorage.setItem("token", data.token); } else { console.error("Login failed:", data.error); } } catch (error) { console.error("Error during login:", error); } };
### .then/.catch
- **Syntax**: Uses chained `.then()` for success and `.catch()` for errors.
- **Error Handling**: Errors are caught in the `.catch()` block.
- **Readability**: Can become harder to read with nested `.then()` calls (callback hell).
**Example**:
```javascript
const handleSubmit = (e) => {
e.preventDefault();
fetch("http://localhost:5000/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
})
.then((response) => response.json())
.then((data) => {
if (data.token) {
console.log("Token received:", data.token);
localStorage.setItem("token", data.token);
} else {
console.error("Login failed:", data.error);
}
})
.catch((error) => {
console.error("Error during login:", error);
});
};
| Feature | async/await |
.then/.catch |
|---|---|---|
| Syntax | Cleaner and more readable | Can become verbose with chaining |
| Error Handling | try/catch blocks |
.catch() method |
| Readability | Easier for sequential logic | Harder with nested .then() calls |
| Use Case | Preferred for modern JavaScript | Useful for simple, quick operations |
- In this single-page application (SPA) setup controlled by setView, there's no functional need to create a separate LogoutForm.jsx
- Unless you're trying to keep your code modular or plan to reuse the logout UI. So, No, LogoutForm.jsx
Frontend Logout:
- The
handleLogoutfunction inApp.jsxremoves the token fromlocalStorageand resets theuserstate.
Example:
const handleLogout = () => {
console.log("Logging out...");
setUser(null);
localStorage.removeItem("token");
localStorage.removeItem("userEmail");
localStorage.removeItem("userRole"); // Remove userRole as well
console.log("Token, userEmail and userRole removed from localStorage");
setView("info");
};
<button className="style of your choice" onClick={handleLogout}>
Logout
</button>;Backend /logout:
-
Currently, there is no
/logoutroute in the backend. This is fine for a stateless JWT-based authentication system. -
If needed, a
/logoutroute could simply return a success message:app.post("/logout", (req, res) => { res.json({ message: "Logged out successfully" }); });
-
Implement Bearer Token Logic:
-
Protect routes like
/api/charactersby requiring a valid token in theAuthorizationheader. -
Example middleware:
const authenticateToken = (req, res, next) => { const token = req.headers["authorization"]?.split(" ")[1]; if (!token) return res.status(401).json({ error: "Access denied" }); jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) return res.status(403).json({ error: "Invalid token" }); req.user = user; next(); }); };
-
-
Frontend Token Usage:
- Include the token in the
Authorizationheader for authenticated requests:const token = localStorage.getItem("token"); fetch("http://localhost:5000/protected-route", { method: "GET", headers: { Authorization: `Bearer ${token}`, }, });
- Include the token in the
-
Enhance Logout:
- Ensure the token is removed from
localStorageand the app state is reset.
- Ensure the token is removed from
-
Optional Backend
/logout:- If you want to add a
/logoutroute, it could simply return a success message. For example:app.post("/logout", (req, res) => { res.json({ message: "Logged out successfully" }); });
- However, this is not strictly necessary unless you want to implement token invalidation.
- If you want to add a
-
Protected Routes:
- Bearer token logic to protect routes like
/api/characters. This ensures only authenticated users can access certain endpoints. - Implement Bearer token logic in the backend to protect routes.
- Update the frontend to include the token in the
Authorizationheader for authenticated requests.
- Bearer token logic to protect routes like
-
Recap of Current Flow:
- Register: User registers via
/registerand gets added to the database. - Login: User logs in via
/login, receives a JWT token, and stores it in localStorage - Logout: User clicks the "Logout" button, which removes the token from localStorage and resets the app state.
- Register: User registers via
When building authentication systems, you can store the JWT token in two main ways: HTTP-only cookies or in the Authorization header (Bearer token).
-
Storage: The token is saved in
localStorageorsessionStorage. -
How It Works: Sent via the
Authorizationheader with every request. -
Example:
fetch("http://localhost:5000/api/characters", { headers: { Authorization: `Bearer ${token}`, }, });
-
Pros:
- Easy to implement.
- Works well for APIs and SPA setups like React apps.
-
Cons:
- Vulnerable to XSS attacks if your site is not secure.
- Developers must manually include the token in each request.
- Storage: Stored in a cookie with HttpOnly and Secure flags.
- How It Works: Automatically sent by the browser with every request to your backend (no manual Authorization header needed).
Set by Backend:
res.cookie("token", jwtToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "Strict",
maxAge: 3600000, // 1 hour
});Pros:
- Immune to XSS (JavaScript cannot access the cookie).
- Safer for storing sensitive tokens.
Cons:
- Slightly more complex to implement.
- CSRF protection needs to be considered.
In production, all users are registered with the default "user" role. Admin accounts should never be exposed via the frontend form.
To register an admin user during development:
- 1.Open Postman or similar API tool.
- 2.Send a POST request to:
http://localhost:5000/register- 3.Use the following request body:**
{
"email": "[email protected]",
"password": "yourSecurePassword",
"role": "admin"
}- 4.If successful, the response should look like:
{
"message": "User registered successfully"
}This method allows you to test requireAdmin middleware and admin-only routes securely in development.
RegisterForm.jsx does not expose the ability to set the user role.
- All users registering through the frontend will always be assigned the "user" role by default, as enforced by the backend.