Skip to content
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
30 changes: 30 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Dependencies
node_modules/
dist/
.next/
.out/
.turbo/

# Local config
.env
.env.local
.DS_Store

# IDE
.vscode/
.idea/

# Logs and caches
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Frontend
frontend/.next/
frontend/out/
frontend/package-lock.json

# Backend
backend/dist/
backend/package-lock.json
backend/.turbo/
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
# INSTRUCTIONS TO RUN THE PROJECT

To run the project properly, follow the steps below:

1. **Install dependencies**
You need to install dependencies in all three folders: `subscriptionTest`, `backend`, and `frontend`:
- yarn install

2. To run both projects, access `subscriptionTest` folder and run the following command (This will run both servers concurrently).
- yarn start

3. Create envirorment variables below:
- **Backend** (backend/.env):
NODE_ENV=development
PORT=3001
MONGO_HOST=localhost
MONGO_PORT=27017
MONGO_DB_NAME=test

- **Frontend** (frontend/.env):
BASE_URL=http://localhost:3000/api
API_URL=http://localhost:3001/api

4. To run the projects seperatly, run the following commands:
- For BACKEND:
cd backend
yarn dev

- For FRONTEND:
cd frontend
yarn dev

# NOTES
- The project structure, environment setup, and `.gitignore` were manually configured.
- The frontend uses the Mantine UI library to help build a modern and responsive user interface.
- This project was designed with scalability and clean code in mind. It includes centralized API handling, global error management, and a shared task context for state management on the frontend.

---------------------------------------------------------------------------------------------------------

BACKEND Instructions

Getting Started
Expand Down
45 changes: 0 additions & 45 deletions backend/dist/index.js

This file was deleted.

7 changes: 5 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"start": "node -r dotenv/config dist/index.js",
"dev": "ts-node-dev --respawn src/index.ts",
"build": "tsc"
},
"dependencies": {
"@types/cors": "^2.8.17",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.18.2",
"mongoose": "^7.0.0"
"mongoose": "^7.0.0",
"ts-node-dev": "^2.0.0"
},
"devDependencies": {
"@types/express": "^4.17.15",
Expand Down
6 changes: 6 additions & 0 deletions backend/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const config = {
env: process.env.NODE_ENV || "development",
port: parseInt(process.env.PORT || "3001"),
};

export default config;
31 changes: 31 additions & 0 deletions backend/src/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import mongoose from "mongoose";
import dotenv from "dotenv";

dotenv.config();

const {
MONGO_USER,
MONGO_PASS,
MONGO_HOST,
MONGO_PORT,
MONGO_DB_NAME,
} = process.env;

if (!MONGO_HOST || !MONGO_PORT || !MONGO_DB_NAME) {
throw new Error("Database configuration is missing in .env");
}

const hasAuth = MONGO_USER && MONGO_PASS;
const MONGO_URI = hasAuth
? `mongodb://${MONGO_USER}:${MONGO_PASS}@${MONGO_HOST}:${MONGO_PORT}/${MONGO_DB_NAME}?authSource=admin`
: `mongodb://${MONGO_HOST}:${MONGO_PORT}/${MONGO_DB_NAME}`;

export const connectToDatabase = async () => {
try {
await mongoose.connect(MONGO_URI);
console.log("Connected to:", MONGO_URI);
} catch (err) {
console.error("Connection error:", err);
throw new Error("Failed to connect to database");
}
};
29 changes: 29 additions & 0 deletions backend/src/errors/HttpError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export class HTTPError extends Error {
status: number;
code: string;

constructor(status: number, message: string, code = "ERROR") {
super(message);
this.status = status;
this.code = code;
this.name = this.constructor.name;
}
}

export class BadRequestError extends HTTPError {
constructor(message = "Bad Request") {
super(400, message, "BAD_REQUEST");
}
}

export class NotFoundError extends HTTPError {
constructor(message = "Not Found") {
super(404, message, "NOT_FOUND");
}
}

export class ForbiddenError extends HTTPError {
constructor(message = "Forbidden") {
super(403, message, "FORBIDDEN");
}
}
62 changes: 19 additions & 43 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,19 @@
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";

const app = express();
app.use(cors());
app.use(bodyParser.json());

mongoose
.connect("mongodb://localhost:27017/test", {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log("Connected to MongoDB"))
.catch((err) => console.error("MongoDB connection error:", err));

interface ITask {
title: string;
completed: boolean;
}

const taskSchema = new Schema<ITask>({
title: { type: String, required: true },
completed: { type: Boolean, default: false },
});

const Task = model<ITask>("Task", taskSchema);

app.post("/task", (req, res) => {
const newTask = new Task({
title: req.body.title,
completed: req.body.completed,
});
newTask.save();
res.status(201).json(newTask);
});

app.get("/tasks", async (req, res) => {
const tasks = Task.find();
res.json(tasks);
});

app.listen(3001, () => console.log("Server running on port 3001"));
import { connectToDatabase } from "./db";
import { createServer } from "./server";
import config from "./config";

const startServer = async () => {
try {
await connectToDatabase();

const server = createServer();
server.listen(config.port, () => {
console.log(`Server running on ${config.port}`);
});
} catch (err) {
console.error("Error connecting to the database:", err);
process.exit(1);
}
};

startServer();
16 changes: 16 additions & 0 deletions backend/src/middleware/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Request, Response, NextFunction } from "express";
import { HTTPError } from "../errors/HttpError";

export function ErrorHandler(err: any, _req: Request, res: Response, _next: NextFunction) {
if (err instanceof HTTPError) {
return res.status(err.status).json({
code: err.code || "ERROR",
message: err.message,
});
}

return res.status(500).json({
code: "INTERNAL_SERVER_ERROR",
message: err.message || "An unknown error occurred.",
});
}
39 changes: 39 additions & 0 deletions backend/src/repositories/tasks/task.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ITask, TaskModel } from "../../routes/tasks/models/task.model";

const LIMIT = 10

export class TaskRepository {
async create(taskData: ITask) {
const task = new TaskModel(taskData);
await task.save();
return task
}

async findAll(page = 1) {
const skip = (page - 1) * LIMIT;

const [tasks, totalItens] = await Promise.all([
TaskModel.find()
.sort({ createdAt: "desc" })
.skip(skip)
.limit(LIMIT),
TaskModel.countDocuments()
]);

const total = Math.ceil(totalItens / LIMIT);

return { tasks, total };
}

async findById(id: string) {
return await TaskModel.findById(id);
}

async update(id: string, taskData: ITask) {
return await TaskModel.findByIdAndUpdate(id, taskData, { new: true, runValidators: true });
}

async delete(id: string) {
return await TaskModel.findByIdAndDelete(id);
}
}
8 changes: 8 additions & 0 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express, { Router } from "express";
import tasks from "./tasks";

const api: Router = express.Router();

api.use("/tasks", tasks);

export default api;
45 changes: 45 additions & 0 deletions backend/src/routes/tasks/controllers/tasks.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Request, Response } from "express";
import { CreateTaskService } from "../services/create.service";
import { ListTasksService } from "../services/list.service";
import { UpdateTaskService } from "../services/update.service";
import { asyncHandler } from "../../../utils/AsyncHandler";
import { DeleteTaskService } from "../services/delete.service";

export const list = async (req: Request, res: Response) => {
try {
const page = parseInt(req.query.page as string) || 1;
const service = new ListTasksService();
const tasks = await service.list(page);
res.status(200).json(tasks);
} catch (err: any) {
res.status(500).json({ message: err.message });
}
};

export const create = async (req: Request, res: Response) => {
try {
const service = new CreateTaskService();
const taskData = req.body;
const newTask = await service.create(taskData);
res.status(201).json(newTask);
} catch (err: any) {
res.status(500).json({ message: err.message });
}
};

export const update = asyncHandler(async (req, res) => {
const service = new UpdateTaskService();
const { id } = req.params;
const taskData = req.body;

const updatedTask = await service.update(id, taskData);
res.status(200).json(updatedTask);
});

export const destroy = asyncHandler(async (req, res) => {
const service = new DeleteTaskService();
const { id } = req.params;

await service.delete(id);
return res.status(200).json({ message: "Task deleted successfully" });
});
11 changes: 11 additions & 0 deletions backend/src/routes/tasks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import express, { Router } from "express";
import { create, destroy, list, update } from "./controllers/tasks.controller";

const tasks: Router = express.Router();

tasks.get("/", list);
tasks.post("/", create);
tasks.put("/:id", update);
tasks.delete("/:id", destroy);

export default tasks;
Loading