Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fantasy Realm API #507

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ node_modules
.env.development.local
.env.test.local
.env.production.local
package-lock.json
package-lock.json
.vercel
173 changes: 166 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,172 @@
# Project Mongo API
# Fantasy World API

Replace this readme with your own information about your project.
A REST API for managing a fantasy realm with characters, quests, items, and worlds.

Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
## **See it live:**

## The problem
- Production: [https://fantasy-world-mongodb-api.vercel.app](https://fantasy-world-mongodb-api.vercel.app)
You may need to refresh a couple of times before the server awakens.

Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next?
---

## View it live
## **Endpoints**

Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about.
### **Worlds**

#### **GET /worlds**

Retrieve a list of all worlds.

**Response:**

```json
[
{
"_id": "675c299a308f20c14cc641f4",
"name": "Middle Earth",
"description": "A mystical land with elves, dwarves, and hobbits."
}
]
```

---

### **Characters**

#### **GET /characters**

Retrieve a list of characters. Supports optional query parameters.

**Query Parameters:**

- `homeWorld`: Filter characters by their home world ID.
- `item`: Filter characters by the item they own.

**Example Request:**

```bash
GET /characters?homeWorld=675c3e604c6a6806a5c1cf7c&item=675c3e604c6a6806a5c1cf92
```

**Response:**

```json
[
{
"_id": "675c299a308f20c14cc641fa",
"name": "Frodo Baggins",
"role": "Hobbit",
"homeWorld": {
"_id": "675c299a308f20c14cc641f4",
"name": "Middle Earth",
"description": "A mystical land with elves, dwarves, and hobbits."
},
"quests": [
{
"_id": "675c299a308f20c14cc64202",
"title": "Destroy the One Ring",
"description": "Carry the One Ring to Mount Doom to save Middle Earth.",
"reward": "Peace for Middle Earth"
}
],
"item": {
"_id": "675c299a308f20c14cc6420c",
"name": "Sting",
"type": "Weapon"
}
}
]
```

#### **POST /characters**

Create a new character.

**Request Body:**

```json
{
"name": "Aragorn",
"role": "Ranger",
"homeWorld": "675c299a308f20c14cc641f4",
"item": "675c299a308f20c14cc6420d",
"quests": ["675c299a308f20c14cc64202"]
}
```

**Response:**

```json
{
"_id": "675c299a308f20c14cc64215",
"name": "Aragorn",
"role": "Ranger",
"homeWorld": "675c299a308f20c14cc641f4",
"item": "675c299a308f20c14cc6420d",
"quests": ["675c299a308f20c14cc64202"]
}
```

---

### **Quests**

#### **GET /quests**

Retrieve a list of all quests.

**Response:**

```json
[
{
"_id": "675c299a308f20c14cc64202",
"title": "Destroy the One Ring",
"description": "Carry the One Ring to Mount Doom to save Middle Earth.",
"reward": "Peace for Middle Earth",
"assignedTo": ["675c299a308f20c14cc641fa", "675c299a308f20c14cc641fb"]
}
]
```

---

### **Items**

#### **GET /items**

Retrieve a list of all items.

**Response:**

```json
[
{
"_id": "675c299a308f20c14cc6420c",
"name": "Sting",
"type": "Weapon",
"owner": "675c299a308f20c14cc641fa"
}
]
```

---

## **Error Handling**

All endpoints return standard HTTP status codes with an error message if something goes wrong.

**Example Error Response:**

```json
{
"error": "Validation error",
"details": [
{
"msg": "Name is required.",
"param": "name",
"location": "body"
}
]
}
```
26 changes: 26 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import express from "express";
import listEndpoints from "express-list-endpoints";
import cors from "cors";
import { worldRoutes } from "./routes/world.routes.js";
import { characterRoutes } from "./routes/character.routes.js";
import { questRoutes } from "./routes/quest.routes.js";
import { itemRoutes } from "./routes/item.routes.js";
import { checkDbConnection } from "./middleware/dbConnection.js";

export const app = express();

// Middleware
app.use(cors());
app.use(express.json());
app.use(checkDbConnection);

// Routes
app.use("/worlds", worldRoutes);
app.use("/characters", characterRoutes);
app.use("/quests", questRoutes);
app.use("/items", itemRoutes);

// API documentation
app.get("/", (req, res) => {
res.json(listEndpoints(app));
});
13 changes: 13 additions & 0 deletions config/database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import mongoose from "mongoose";

export const connectDatabase = async () => {
const mongoUrl = process.env.MONGO_URL;
// const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/fantasy-world";

try {
await mongoose.connect(mongoUrl);
console.log("Database connected successfully!");
} catch (error) {
console.error("Database connection failed:", error);
}
};
65 changes: 65 additions & 0 deletions controllers/character.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Character } from "../models/character.model.js";

export const getCharacters = async (req, res) => {
try {
// Extract query params
const { homeWorld, item } = req.query;

const filter = {};
if (homeWorld) filter.homeWorld = homeWorld;
if (item) filter.item = item;

const characters = await Character.find(filter)
.populate("homeWorld")
.populate("quests")
.populate("item");
res.status(200).json(characters);
} catch (error) {
res.status(500).json({ error: "Failed to fetch characters" });
}
};

export const createCharacter = async (req, res) => {
try {
// Validate required fields
const { name, role, homeWorld } = req.body;

// Collect missing fields dynamically
const missingFields = [];
if (!name) missingFields.push("name");
if (!role) missingFields.push("role");
if (!homeWorld) missingFields.push("homeWorld");

// If there are missing fields, return a 400 error
if (missingFields.length > 0) {
return res.status(400).json({
error: `Missing required fields: ${missingFields.join(", ")}`,
});
}

// Create and save the character
const character = new Character(req.body);
await character.save();
res.status(201).json(character);
} catch (error) {
// Check for validation errors from Mongoose
if (error.name === "ValidationError") {
const errors = Object.values(error.errors).map((err) => err.message);
return res
.status(400)
.json({ error: "Validation error", details: errors });
}

// Check for MongoDB duplicate key errors
if (error.code === 11000) {
return res
.status(400)
.json({ error: "Duplicate key error", details: error.keyValue });
}

// General server error
res
.status(500)
.json({ error: "Internal server error", details: error.message });
}
};
20 changes: 20 additions & 0 deletions controllers/item.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Item } from "../models/item.model.js";

export const getItems = async (req, res) => {
try {
const items = await Item.find().populate("owner");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this tip ⭐

res.status(200).json(items);
} catch (error) {
res.status(500).json({ error: "Failed to fetch items" });
}
};

export const createItem = async (req, res) => {
try {
const item = new Item(req.body);
await item.save();
res.status(201).json(item);
} catch (error) {
res.status(400).json({ error: "Failed to create item" });
}
};
20 changes: 20 additions & 0 deletions controllers/quest.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Quest } from "../models/quest.model.js";

export const getQuests = async (req, res) => {
try {
const quests = await Quest.find().populate("assignedTo");
res.status(200).json(quests);
} catch (error) {
res.status(500).json({ error: "Failed to fetch quests" });
}
};

export const createQuest = async (req, res) => {
try {
const quest = new Quest(req.body);
await quest.save();
res.status(201).json(quest);
} catch (error) {
res.status(400).json({ error: "Failed to create quest" });
}
};
20 changes: 20 additions & 0 deletions controllers/world.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { World } from "../models/world.model.js";

export const getWorlds = async (req, res) => {
try {
const worlds = await World.find();
res.status(200).json(worlds);
} catch (error) {
res.status(500).json({ error: "Failed to fetch worlds" });
}
};

export const createWorld = async (req, res) => {
try {
const world = new World(req.body);
await world.save();
res.status(201).json(world);
} catch (error) {
res.status(400).json({ error: "Failed to create world" });
}
};
Loading