/How

❓🗣️ Robust Q&A application built with NestJS. Empowering users with knowledge, seamless communication, and comprehensive features.

Primary LanguageTypeScriptMIT LicenseMIT

how-high-resolution-logo-color-on-transparent-background




📑 Table of Contents

📘 Introduction

Welcome to the How Backend project! Built with NestJS, a progressive Node.js framework, How is a robust and efficient Q&A application designed to empower users with knowledge and facilitate seamless communication. The How provides a SOLID foundation for building a feature-rich Q&A platform, where users can share knowledge, engage in discussions, and expand their understanding. This project consists of five modules, each serving a specific purpose to deliver a comprehensive user experience.

The authentication module provides secure user registration, login, and password reset functionality. With guards ensuring authentication and convenient decorators like @currentUser, accessing user information is a breeze. The email module integrates Nodemailer for reliable email communication, allowing users to stay connected effortlessly. The follow module enables users to connect with others, fostering a vibrant community. The question module empowers users to create, update, and delete questions, while the answer module facilitates answering and managing questions effectively. With a focus on data management, TypeORM is utilized to define entities and establish relationships between them, simplifying database operations.

With serialization and interception powered by the SerializeInterceptor, sensitive information is automatically excluded from outgoing responses, ensuring data privacy.

To optimize performance, caching has been implemented using Redis. The @nestjs/cache-manager package is used for caching, with Redis as the database store. The Cache-Aside Pattern and TTL (Time to Live) strategy are employed for cache management, improving response times and reducing database load.

(back to top)

🚀 Live Demo

💻 Getting Started

To get a local copy up and running, follow these steps.

Prerequisites ❗

In order to run this project you need:

Environment Variables 🔑

To run this project, you will need to add the following environment variables to a new file at the root directory named .env:

  • HOST: the host of your project (e.g. localhost)
  • PORT: the port of which your project work on (e.g. 3000)
  • DATABASE_URL: the postgres connection string postgres://username:password@host:port/databsename (e.g postgres://postgres:root@localhost:5432/How)
  • REDIS_URL: the redis connection string redis://host:port (e.g redis://localhost:6379)
  • JWT_SECRET: the json web token signature to create or validate token (e.g. jwtsecret)
  • NODEMAILER_EMAIL: the gmail account you will use to forward email (e.g. your-email@gmail.com)
  • NODEMAILER_PASSWORD: you should SMTP server password form you gmail and enable you 2-step verficaiotn (watch this video to get your password)
  • COOKIE_SESSION_SECRET: your cookie session secret (e.g sessionsecret)

Setup ⬇️

  1. Clone the repository:
   git clone https://github.com/ahmedeid6842/How
  1. Change to the project directory:
cd ./How

Install ✔️

Install the project dependencies using NPM:

npm install

Usage 🤿 🏃‍♂️

To start the application in development mode, run the following command:

npm run start:dev

The application will be accessible at http://localhost:3000.

  • Alright, it's showtime! 🔥 Hit http://localhost:3000 and BOOM! 💥 You should see the docs page and the HOW APIs working flawlessly. ✨🧙‍♂️

(back to top)

Authentication

Follow

Question

Answer

🏗️🔨 Database ERD

ERD-V2

🔄 Sequence Diagrams

Auth Module

sequenceDiagram
    participant User
    participant AuthController
    participant AuthService
    participant UsersService
    participant EmailService
    participant JwtService

    User->>+AuthController: register()
    AuthController->>+AuthService: register(userCredentials)
    AuthService->>+UsersService: createUser(userCredentials)
    UsersService-->>-AuthService: user
    AuthService->>+EmailService: sendRegistrationEmail(user)
    EmailService-->>-AuthService: emailSent
    AuthService-->>-AuthController: registrationSuccess

    User->>+AuthController: login(credentials)
    AuthController->>+AuthService: login(credentials)
    AuthService->>+UsersService: getUserByEmail(email)
    UsersService-->>-AuthService: user
    AuthService->>+AuthService: comparePasswords(password, user.password)
    AuthService->>+JwtService: generateToken(user)
    JwtService-->>-AuthService: token
    AuthService-->>-AuthController: loginSuccess(token)

    User->>+AuthController: requestPasswordReset(email)
    AuthController->>+AuthService: requestPasswordReset(email)
    AuthService->>+UsersService: getUserByEmail(email)
    UsersService-->>-AuthService: user
    AuthService->>+AuthService: generatePasswordResetToken(user)
    AuthService->>+EmailService: sendPasswordResetEmail(user, resetToken)
    EmailService-->>-AuthService: emailSent
    AuthService-->>-AuthController: passwordResetEmailSent()

    User->>+AuthController: resetPassword(resetToken, newPassword)
    AuthController->>+AuthService: resetPassword(resetToken, newPassword)
    AuthService->>+AuthService: verifyPasswordResetToken(resetToken)
    AuthService->>+UsersService: getUserById(userId)
    UsersService-->>-AuthService: user
    AuthService->>+AuthService: hashPassword(newPassword)
    AuthService->>+UsersService: updatePassword(user, hashedPassword)
    UsersService-->>-AuthService: updatedUser
    AuthService-->>-AuthController: passwordResetSuccess()

    User->>+AuthController: verifyEmail(email, verificationCode)
    AuthController->>+AuthService: verifyEmail(email, verificationCode)
    AuthService->>+UsersService: getUserByEmail(email)
    UsersService-->>-AuthService: user
    AuthService->>+AuthService: verifyEmail(user, verificationCode)
    AuthService->>+UsersService: updateUserVerification(user)
    UsersService-->>-AuthService: updatedUser
    AuthService-->>-AuthController: emailVerificationSuccess()

    User->>+AuthController: logout()
    AuthController->>+AuthService: logout()
    AuthService-->>-AuthController: logoutSuccess()
Loading

(back to top)

Follow Module

sequenceDiagram
    participant Client
    participant FollowController
    participant FollowService
    participant UserRepository
    participant UserService

Client->FollowController: POST /follow
FollowController->FollowService: followUser(following_id, follower)
FollowService->UserService: findOne(followingId)
UserService-->FollowService: following
alt Invalid user id
    FollowService-->FollowController: Throw BadRequestException("Invalid user id")
else
    FollowService->FollowService: followExist(followingId, follower.id)
    FollowService->UserService: findOne(follower.id)
    UserService-->FollowService: follower
    alt You can't follow yourself
        FollowService-->FollowController: Throw BadRequestException("you can't follow yourself")
    else
        alt You already a follower
            FollowService-->FollowController: Throw BadRequestException("you already a follower")
        else
            FollowService->UserRepository: create(following, follower)
            UserRepository-->FollowService: follow
            FollowService->UserRepository: save(follow)
            UserRepository-->FollowService: savedFollow
            FollowService-->FollowController: savedFollow
        end
    end
end

Client->FollowController: GET /follow/followers/:id
FollowController->FollowService: getUserFollowers(userId)
FollowService->UserRepository: find({ user: { id: userId } })
UserRepository-->FollowService: follows
FollowService-->FollowController: follows

Client->FollowController: GET /follow/following/:id
FollowController->FollowService: getUserFollowing(userId)
FollowService->UserRepository: find({ follower: { id: userId } })
UserRepository-->FollowService: follows
FollowService-->FollowController: follows

Client->FollowController: PATCH /follow/unfollow
FollowController->FollowService: unFollowUser(following_id, follower)
FollowService->UserService: findOne(followingId)
UserService-->FollowService: following
alt Invalid user id
    FollowService-->FollowController: Throw BadRequestException("Invalid user id")
else
    FollowService->FollowService: followExist(followingId, follower.id)
    alt You are not following this user
        FollowService-->FollowController: Throw BadRequestException("You are not following this user")
    else
        FollowService->UserRepository: remove(follow)
        UserRepository-->FollowService: removedFollow
        FollowService-->FollowController: removedFollow
    end
end
Loading

(back to top)

Question Module

sequenceDiagram
  participant Client
  participant Controller
  participant Service
  participant Repository
  participant QuestionLikesService

  Client->>Controller: POST /question
  Controller->>Service: createQuestion()
  alt Unique question check
    Service->>Service: getQuestion({ title })
    Service->>Repository: queryBuilder.getMany()
    Repository->>Service: questions
    alt Question not unique
      Service->>Controller: BadRequestException
    else
      Service->>Repository: create()
      Repository->>Service: savedQuestion
    end
  else
    Service->>Repository: create()
    Repository->>Service: savedQuestion
  end

  Service->>Service: addQuestion()

  Client->>Controller: GET /question
  Controller->>Service: getQuestion()
  Service->>Repository: queryBuilder.getMany()
  Repository->>Service: questions
  alt No questions found
    Service->>Controller: NotFoundException
  else
    Service->>Controller: questions
  end

  Client->>Controller: PATCH /question/:questionId
  Controller->>Service: updateQuestion()
  Service->>Repository: save()
  Repository->>Service: updatedQuestion

  Client->>Controller: DELETE /question/:questionId
  Controller->>Service: deleteQuestion()
  Service->>Repository: remove()

  Client->>Controller: PATCH /question/like/:questionId
  Controller->>Service: likeQuestion()
  Service->>Service: getQuestion()
  Service->>Repository: queryBuilder.getMany()
  Repository->>Service: questions
  alt Question not found
    Service->>Controller: NotFoundException
  else
    Service->>QuestionLikesService: getLike()
    QuestionLikesService->>Repository: findOne()
    Repository->>QuestionLikesService: like
    alt Like exists
      Service->>Controller: BadRequestException
    else
      QuestionLikesService->>Repository: create()
      Repository->>QuestionLikesService: like
      QuestionLikesService->>Repository: save()
    end
    Service->>Repository: save()
    Repository->>Service: question
  end
Loading

(back to top)

Answer Module

sequenceDiagram
    participant Client
    participant Controller
    participant Service
    participant Repository
    participant QuestionService
    participant AnswerLikesService

    Client->>Controller: POST /answer/:questionId
    Controller->>Service: createAnswer(questionId, body, user)
    Service->>QuestionService: getQuestion({ questionId })
    QuestionService-->>Service: questionExist
    alt questionExist is null
        Service->>Controller: throw NotFoundException
    else questionExist is not null
        Service->>Repository: create(answer, questionExist, user)
        Repository-->>Service: savedAnswer
    end

    Client->>Controller: GET /answer/?query
    Controller->>Service: getAnswer(query)
    Service->>Repository: queryBuilder.getMany()
    Repository-->>Service: answers
    alt answers is empty
        Service->>Controller: throw NotFoundException
    else answers is not empty
        Service-->>Controller: answers
    end

    Client->>Controller: PATCH /answer/:answerId
    Controller->>Service: updateAnswer(answer, body)
    Service->>Repository: save(answer)
    Repository-->>Service: updatedAnswer
    Service-->>Controller: updatedAnswer

    Client->>Controller: DELETE /answer/:questionId/:answerId
    Controller->>Service: deleteAnswer(answer)
    Service->>Repository: remove(answer)

    Client->>Controller: PATCH /answer/like/:answerId
    Controller->>Service: likeAnswer(answerId, user)
    Service->>Service: getAnswer({ answerId })
    Service->>AnswerLikesService: getLike(answerId, user.id)
    AnswerLikesService-->>Service: likeExists
    alt likeExists is not null
        Service->>Controller: throw BadRequestException
    else likeExists is null
        Service->>AnswerLikesService: addLike(answer, user)
        Service->>Repository: save(answer)
    end

Loading

(back to top)

Email Module

sequenceDiagram
    participant Client
    participant EmailService
    participant nodemailer

    Client->>EmailService: sendResetPasswordEmail(email, resetPasswordUrl)
    EmailService->>nodemailer: createTransport(options)
    nodemailer-->>EmailService: transporter
    EmailService->>nodemailer: sendMail(message)
    nodemailer-->>EmailService: result
    EmailService-->>Client: result
Loading

(back to top)

📐 UML Diagram

classDiagram
    class UsersService {
        - userRepo
        + create()
        + findOne()
        + find()
        + update()
    }

    class AuthService {
        - userService
        - emailService
        + register()
        + login()
        + sendResetPasswordEmail()
        + resetPassword()
    }

    class FollowService {
        - followRepo
        - userService
        + startUserFollowing()
        + getUserFollowers()
        + getUserFollowing()
        + unFollowUser()
    }

    class QuestionService {
        - questionRepository
        - questionLikesService
        + addQuestion()
        + getQuestion()
        + updateQuestion()
        + deleteQuestion()
        + likeQuestion()
    }

    class QuestionLikesService {
        - questionLikesRepository
        + getLike()
        + addLike()
    }

    class EmailService {
        - transporter
        - email
        - password
        + sendResetPasswordEmail()
    }

    class AnswerService {
        - questionService
        - answerRepository
        - answerLikeService
        + createAnswer()
        + getAnswer()
        + updateAnswer()
        + deleteAnswer()
        + likeAnswer()
    }

    class AnswerLikesService {
        - answerLikesRepository
        + getLike()
        + addLike()
    }

    

    AuthService --> EmailService : depends on
    AuthService --> UsersService : depends on
    FollowService --> UsersService : depends on
    QuestionService --> QuestionLikesService : depends on
    AnswerService --> QuestionService : depends on
    AnswerService --> AnswerLikesService : depends on
Loading

👤 Author

Ahmed Eid 🙋‍♂️

(back to top)

🤝 Contributing

We're always looking to improve this project! 🔍 If you notice any issues or have ideas for new features, please don't hesitate to submit a pull request 🙌 or create a new issue 💡. Your contribution will help make this project even better! ❤️ 💪

⭐️ Show your support

If you find this project helpful, I would greatly appreciate it if you could leave a star! 🌟 💟

🔭 Up next

  • Implement Search engine for different question searches
  • Support pagination for getting questions
  • Enhance the DataBase queries time by using redis LRU caching
  • Move from monolithic to microservices architecture.
  • Apply Background jobs and task scheduling Use a job queue system like Bull or Agenda to handle time-consuming tasks.

💎 Lessons Learned

  1. Secure user access with effective authentication and authorization.
  2. Use a well-structured architecture, such as Nest.js, for code organization, scalability, and maintainability.
  3. Take advantage of different NestJS components and decorators.
  4. There is something new to learn.

(back to top)

📜 License

This project is licensed under the MIT License - you can click here to have more details MIT licensed.

(back to top)