DEV Community

Piyush Kacha
Piyush Kacha

Posted on

Introduce the NestJS starter kit - Release v0.0.4 πŸš€

In the fast-paced world of web development, building a scalable and secure application from scratch can be daunting. This is where boilerplates and starter kits come in handyβ€”they provide a solid foundation, allowing developers to focus on building features rather than reinventing the wheel.

Today, we're excited to introduce the latest release of our NestJS Starter Kit. In this update, we've added powerful new features that make it easier than ever to jump-start your SaaS application development.

What's New in v0.0.4?

In this release, we've focused on enhancing the starter kit's capabilities to help developers get a jump-start on building SaaS applications. Here are the major updates:

  • Advanced Authentication Module: Secure your application with JWT-based authentication using private and public keys, enhancing both security and flexibility.
  • User and Workspace Modules: Pre-built modules to manage users and collaborative workspaces, essential for any SaaS application.
  • Mongoose Integration: Simplified data modeling and schema management with Mongoose, providing a robust foundation for handling MongoDB databases.

This guide will walk you through the new features and provide step-by-step instructions on setting up your project.

Step 1: Clone the Repository

git clone https://github.com/piyush-kacha/nestjs-starter-kit.git
cd nestjs-starter-kit
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Dependencies

npm install
Enter fullscreen mode Exit fullscreen mode

Step 3: look README.md & Set Up Environment Variables

Create a .env file in the root directory of your project and add the required environment variables. Here’s an example of what your .env file should look like:

## Environment variables for the application

# The environment the application is running in (e.g. development, production, test)
NODE_ENV=

# The port number that the application listens on
PORT=

# The hostname or IP address that the application binds to (e.g. localhost, 127.0.0.1)
HOST=

# The logging level for the application (e.g. debug, info, warn, error)
LOG_LEVEL=

# Whether to enable clustering mode for the application (true or false)
CLUSTERING=

# The URI for the MongoDB database used by the application
MONGODB_URI=

# The private key for generating JSON Web Tokens (JWTs)
# in README.md for more information how to generate a private key
JWT_PRIVATE_KEY=

# The public key for verifying JSON Web Tokens (JWTs)
# in README.md for more information how to generate a public key
JWT_PUBLIC_KEY=

# The expiration time for JSON Web Tokens (JWTs)
# expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js).
#  Eg: 60, "2 days", "10h", "7d"
JWT_EXPIRATION_TIME=
Enter fullscreen mode Exit fullscreen mode

Auth Module

The Auth Module is responsible for handling authentication in the NestJS Starter Kit. It uses JWT (JSON Web Tokens) with Passport.js to secure the application and manage user sessions.

Basic Idea

  • JWT Authentication: The module uses JWTs for authenticating users. Tokens are signed with a private key and verified with a public key, ensuring secure communication.
  • Passport Integration: Passport.js is used for managing authentication strategies. The default strategy is set to JWT.
  • Dynamic Configuration: JWT settings (such as keys and token expiration) are dynamically loaded from environment variables using ConfigService.
  • User and Workspace Modules: The UserModule and WorkspaceModule are imported to manage user and workspace-related data.

Key Components

  • JwtUserStrategy: Custom strategy for handling JWT authentication.
  • AuthService: Contains business logic for authentication, like user login and token generation.
  • AuthController: Handles HTTP requests related to authentication, such as login and registration.

Code Overview

Here's a simple breakdown of what each part does:

  1. Imports Required Libraries: Sets up necessary libraries and modules (ConfigModule, JwtModule, PassportModule).
  2. Defines the Module: Registers strategies, controllers, and services required for authentication.
  3. Exports: Makes the authentication strategy and Passport module available for use in other parts of the application.

With these components, the Auth Module provides a secure, scalable authentication system that can be easily integrated into any NestJS application.

auth.module.ts

// Importing the required libraries
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';

// Importing the required internal files
import { JwtUserStrategy } from './strategies/jwt-user.strategy';

// Importing the required external modules and files
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserModule } from '../user/user.module';
import { WorkspaceModule } from '../workspace/workspace.module';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.registerAsync({
      inject: [ConfigService],
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        privateKey: configService.get('jwt.privateKey'),
        publicKey: configService.get('jwt.publicKey'),
        signOptions: { expiresIn: configService.get('jwt.expiresIn'), algorithm: 'RS256' },
        verifyOptions: {
          algorithms: ['RS256'],
        },
      }),
    }),
    UserModule,
    WorkspaceModule,
  ],
  providers: [JwtUserStrategy, AuthService],
  controllers: [AuthController],
  exports: [JwtUserStrategy, PassportModule],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

auth.controller.ts

import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
import { Body, Controller, HttpCode, Post, ValidationPipe } from '@nestjs/common';

import { AuthService } from './auth.service';
import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos';

import { BadRequestException } from '../../exceptions/bad-request.exception';
import { InternalServerErrorException } from '../../exceptions/internal-server-error.exception';
import { UnauthorizedException } from '../../exceptions/unauthorized.exception';

@ApiBadRequestResponse({
  type: BadRequestException,
})
@ApiInternalServerErrorResponse({
  type: InternalServerErrorException,
})
@ApiUnauthorizedResponse({
  type: UnauthorizedException,
})
@ApiTags('Auth')
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  // POST /auth/signup
  @ApiOkResponse({
    type: SignupResDto,
  })
  @HttpCode(200)
  @Post('signup')
  async signup(@Body(ValidationPipe) signupReqDto: SignupReqDto): Promise<SignupResDto> {
    return this.authService.signup(signupReqDto);
  }

  // POST /auth/login
  @ApiOkResponse({
    type: LoginResDto,
  })
  @HttpCode(200)
  @Post('login')
  async login(@Body(ValidationPipe) loginReqDto: LoginReqDto): Promise<LoginResDto> {
    return this.authService.login(loginReqDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

auth.service.ts

// External dependencies
import * as bcrypt from 'bcryptjs';
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

// Internal dependencies
import { JwtUserPayload } from './interfaces/jwt-user-payload.interface';
import { LoginReqDto, LoginResDto, SignupReqDto, SignupResDto } from './dtos';

// Other modules dependencies
import { User } from '../user/user.schema';
import { UserQueryService } from '../user/user.query.service';
import { WorkspaceQueryService } from '../workspace/workspace.query-service';

// Shared dependencies
import { BadRequestException } from '../../exceptions/bad-request.exception';
import { UnauthorizedException } from '../../exceptions/unauthorized.exception';

@Injectable()
export class AuthService {
  private readonly SALT_ROUNDS = 10;

  constructor(
    private readonly userQueryService: UserQueryService,
    private readonly workspaceQueryService: WorkspaceQueryService,
    private readonly jwtService: JwtService,
  ) {}

  async signup(signupReqDto: SignupReqDto): Promise<SignupResDto> {
    const { email, password, workspaceName, name } = signupReqDto;

    const user = await this.userQueryService.findByEmail(email);
    if (user) {
      throw BadRequestException.RESOURCE_ALREADY_EXISTS(`User with email ${email} already exists`);
    }

    const workspacePayload = {
      name: workspaceName,
    };
    const workspace = await this.workspaceQueryService.create(workspacePayload);

    // Hash password
    const saltOrRounds = this.SALT_ROUNDS;
    const hashedPassword = await bcrypt.hash(password, saltOrRounds);

    const userPayload: User = {
      email,
      password: hashedPassword,
      workspace: workspace._id,
      name,
      verified: true,
      registerCode: this.generateCode(),
      verificationCode: null,
      verificationCodeExpiry: null,
      resetToken: null,
    };

    await this.userQueryService.create(userPayload);

    return {
      message: 'User created successfully',
    };
  }

  /**
   * Generates a random six digit OTP
   * @returns {number} - returns the generated OTP
   */
  generateCode(): number {
    const OTP_MIN = 100000;
    const OTP_MAX = 999999;
    return Math.floor(Math.random() * (OTP_MAX - OTP_MIN + 1)) + OTP_MIN;
  }

  async login(loginReqDto: LoginReqDto): Promise<LoginResDto> {
    const { email, password } = loginReqDto;

    const user = await this.userQueryService.findByEmail(email);
    if (!user) {
      throw UnauthorizedException.UNAUTHORIZED_ACCESS('Invalid credentials');
    }

    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      throw UnauthorizedException.UNAUTHORIZED_ACCESS('Invalid credentials');
    }

    const payload: JwtUserPayload = {
      user: user._id,
      email: user.email,
      code: user.registerCode,
    };
    const accessToken = await this.jwtService.signAsync(payload);

    delete user.password;

    return {
      message: 'Login successful',
      accessToken,
      user,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

jwt-user.strategy.ts

import { ConfigService } from '@nestjs/config';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';

import { JwtUserPayload } from '../interfaces/jwt-user-payload.interface';
import { UnauthorizedException } from '../../../exceptions/unauthorized.exception';
import { UserQueryService } from '../../user/user.query.service';

@Injectable()
export class JwtUserStrategy extends PassportStrategy(Strategy, 'authUser') {
  constructor(
    private readonly configService: ConfigService,
    private readonly userQueryService: UserQueryService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: configService.get('jwt.publicKey'),
    });
  }

  // validate method is called by passport-jwt when it has verified the token signature
  async validate(payload: JwtUserPayload) {
    const user = await this.userQueryService.findById(payload.user);
    if (!user) {
      throw UnauthorizedException.UNAUTHORIZED_ACCESS();
    }
    if (!user.verified) {
      throw UnauthorizedException.USER_NOT_VERIFIED();
    }
    if (payload.code !== user.registerCode) {
      throw UnauthorizedException.REQUIRED_RE_AUTHENTICATION();
    }
    delete user.password; // remove password from the user object
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

jwt-user-auth.guard.ts

import { AuthGuard } from '@nestjs/passport';
import { ExecutionContext, Injectable } from '@nestjs/common';

import { UnauthorizedException } from '../../../exceptions/unauthorized.exception';

@Injectable()
export class JwtUserAuthGuard extends AuthGuard('authUser') {
  JSON_WEB_TOKEN_ERROR = 'JsonWebTokenError';

  TOKEN_EXPIRED_ERROR = 'TokenExpiredError';

  canActivate(context: ExecutionContext) {
    // Add your custom authentication logic here
    // for example, call super.logIn(request) to establish a session.
    return super.canActivate(context);
  }

  handleRequest(err: any, user: any, info: Error, context: any, status: any) {
    // You can throw an exception based on either "info" or "err" arguments
    if (info?.name === this.JSON_WEB_TOKEN_ERROR) {
      throw UnauthorizedException.JSON_WEB_TOKEN_ERROR();
    } else if (info?.name === this.TOKEN_EXPIRED_ERROR) {
      throw UnauthorizedException.TOKEN_EXPIRED_ERROR();
    } else if (info) {
      throw UnauthorizedException.UNAUTHORIZED_ACCESS(info.message);
    } else if (err) {
      throw err;
    }

    return super.handleRequest(err, user, info, context, status);
  }
}
Enter fullscreen mode Exit fullscreen mode

The Auth Module in this NestJS Starter Kit provides an authentication system using JWT (JSON Web Tokens) and Passport.js. It handles user sign-up and login processes securely, utilizing private and public keys for token signing and verification. The module is designed for flexibility and security, featuring dynamic configuration loading via environment variables and integrating with user and workspace management for a seamless SaaS application experience.

Key components include the AuthService for business logic, AuthController for handling HTTP requests, and JwtUserStrategy for custom JWT authentication strategy. The setup ensures secure communication and easy extensibility, making it an ideal choice for modern web applications.

User Module

The User Module in the NestJS Starter Kit is designed to handle all user-related operations, including user creation, retrieval, and management. This module integrates with MongoDB using Mongoose for data persistence and provides a set of services, controllers, and repository patterns to efficiently manage user data.

Overview of the User Module

The User Module provides a robust framework for managing users within a SaaS application. It includes functionalities for CRUD operations on user data, along with user authentication and authorization integrations. This module is an essential part of the application's infrastructure, supporting both user-specific actions and broader business logic that involves users.

Key Components

  • UserModule: The main module that imports necessary components and provides services and controllers for user management.

    • MongooseModule.forFeature: Registers the User schema with Mongoose, enabling MongoDB operations on the User collection.
  • UserController: Exposes HTTP endpoints for user-related operations. It includes endpoints like getting the user profile and integrates JWT-based authentication guards for securing these endpoints.

    • ApiTags and ApiBearerAuth: Used for Swagger documentation to indicate the controller's purpose and required authorization.
    • GetProfileResDto: Defines the response structure for retrieving user profiles.
    • JwtUserAuthGuard: A guard that protects the routes, ensuring only authenticated users can access them.
  • UserQueryService: Provides a service layer to interact with the UserRepository for querying user data. This service is responsible for:

    • Finding Users: By email or ID.
    • Creating Users: Manages the creation of new user records.
    • Error Handling: Catches and processes exceptions to ensure robustness.
  • UserRepository: Directly interacts with the MongoDB database using Mongoose to perform CRUD operations. It abstracts the database logic from the rest of the application, providing methods for:

    • Finding Users: Both single and multiple user queries.
    • Updating Users: Find and update operations.
    • Creating Users: Inserting new user records into the database.
  • UserSchema: Defines the schema for the User collection in MongoDB using Mongoose. The schema includes fields like email, password, name, workspace, verification status, and more.

  • Schema Decorators: Utilizes NestJS and Mongoose decorators to define schema properties, validation rules, and indexes.

    • UserDocument: A type that represents a user document within the MongoDB collection.

How It Works

  1. Registration: When a new user registers, the UserController receives the request and passes it to the AuthService (from the Auth Module) for processing. The user information is then sent to the UserQueryService for creation in the database.

  2. Authentication and Authorization: The module integrates with the Auth Module to use JWT-based authentication, ensuring that all endpoints are protected and only accessible to authorized users.

  3. User Data Management: The UserQueryService and UserRepository work together to manage user data, providing a clean separation between business logic and database operations.

  4. Error Handling and Logging: The module is equipped with robust error handling to manage database and operational errors, ensuring smooth and secure user data operations. The UserController uses the Logger for logging user-related actions.

user.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { DatabaseCollectionNames } from '../../shared/enums/db.enum';
import { UserController } from './user.controller';
import { UserQueryService } from './user.query.service';
import { UserRepository } from './user.repository';
import { UserSchema } from './user.schema';

@Module({
  imports: [MongooseModule.forFeature([{ name: DatabaseCollectionNames.USER, schema: UserSchema }])],
  providers: [UserQueryService, UserRepository],
  exports: [UserQueryService],
  controllers: [UserController],
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

user.controller.ts

// External dependencies
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { Controller, Get, HttpCode, Logger, UseGuards } from '@nestjs/common';

// Internal dependencies
import { GetProfileResDto } from './dtos';
import { UserDocument } from './user.schema';

// Other modules dependencies
import { GetUser } from '../auth/decorators/get-user.decorator';
import { JwtUserAuthGuard } from '../auth/guards/jwt-user-auth.guard';

@ApiBearerAuth()
@ApiTags('User')
@UseGuards(JwtUserAuthGuard)
@Controller('user')
export class UserController {
  private readonly logger = new Logger(UserController.name);

  // GET /user/me
  @HttpCode(200)
  @ApiOkResponse({
    type: GetProfileResDto,
  })
  @Get('me')
  async getFullAccess(@GetUser() user: UserDocument): Promise<GetProfileResDto> {
    this.logger.debug(`User ${user.email} requested their profile`);
    return {
      message: 'Profile retrieved successfully',
      user,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

user.query.service.ts

// Objective: Implement the user query service to handle the user queries
// External dependencies
import { Injectable } from '@nestjs/common';

// Internal dependencies
import { User, UserDocument } from './user.schema';
import { UserRepository } from './user.repository';

// Other modules dependencies

// Shared dependencies
import { Identifier } from '../../shared/types/schema.type';
import { InternalServerErrorException } from '../../exceptions/internal-server-error.exception';

@Injectable()
export class UserQueryService {
  constructor(private readonly userRepository: UserRepository) {}

  // findByEmail is a method that finds a user by their email address
  async findByEmail(email: string): Promise<User> {
    try {
      return await this.userRepository.findOne({ email });
    } catch (error) {
      throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error);
    }
  }

  // findById is a method that finds a user by their unique identifier
  async findById(id: Identifier): Promise<User> {
    try {
      return await this.userRepository.findById(id);
    } catch (error) {
      throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error);
    }
  }

  // create is a method that creates a new user
  async create(user: User): Promise<UserDocument> {
    try {
      return await this.userRepository.create(user);
    } catch (error) {
      throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

user.repository.ts

// Purpose: User repository for user module.
// External dependencies
import { FilterQuery, Model, QueryOptions, Types, UpdateQuery } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Injectable } from '@nestjs/common';

// Internal dependencies
import { User, UserDocument } from './user.schema';

// Shared dependencies
import { DatabaseCollectionNames } from '../../shared/enums/db.enum';

@Injectable()
export class UserRepository {
  constructor(@InjectModel(DatabaseCollectionNames.USER) private userModel: Model<UserDocument>) {}

  async find(filter: FilterQuery<UserDocument>): Promise<User[]> {
    return this.userModel.find(filter).lean();
  }

  async findById(id: string | Types.ObjectId): Promise<User | null> {
    return this.userModel.findById(id).lean();
  }

  async findOne(filter: FilterQuery<UserDocument>): Promise<User | null> {
    return this.userModel.findOne(filter).lean();
  }

  async create(user: User): Promise<UserDocument> {
    return this.userModel.create(user);
  }

  async findOneAndUpdate(
    filter: FilterQuery<UserDocument>,
    update: UpdateQuery<UserDocument>,
    options: QueryOptions<UserDocument>,
  ): Promise<UserDocument | null> {
    return this.userModel.findOneAndUpdate(filter, update, options);
  }

  async findByIdAndUpdate(id, update: UpdateQuery<UserDocument>, options: QueryOptions<UserDocument>): Promise<UserDocument | null> {
    return this.userModel.findByIdAndUpdate(id, update, options);
  }
}
Enter fullscreen mode Exit fullscreen mode

user.schema.ts

import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';

import { HydratedDocument, Schema as MongooseSchema, Types } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';

import { DatabaseCollectionNames } from '../../shared/enums';
import { Identifier } from '../../shared/types';

@Schema({
  timestamps: true,
  collection: DatabaseCollectionNames.USER,
})
export class User {
  // _id is the unique identifier of the user
  @ApiProperty({
    description: 'The unique identifier of the user',
    example: '643405452324db8c464c0584',
  })
  @Prop({
    type: MongooseSchema.Types.ObjectId,
    default: () => new Types.ObjectId(),
  })
  _id?: Types.ObjectId;

  // email is the unique identifier of the user
  @ApiProperty({
    description: 'The unique identifier of the user',
    example: 'john@example.com',
  })
  @Prop({
    required: true,
  })
  email: string;

  // password is the hashed password of the user
  @ApiHideProperty()
  @Prop()
  password?: string;

  // workspace is the unique identifier of the workspace that the user belongs to
  @ApiProperty({
    description: 'The unique identifier of the workspace',
    example: '643405452324db8c464c0584',
  })
  @Prop({
    type: MongooseSchema.Types.ObjectId,
    ref: DatabaseCollectionNames.WORKSPACE,
  })
  workspace: Identifier;

  // name is the full name of the user
  @ApiProperty({
    description: 'The full name of the user',
    example: 'John Doe',
  })
  @Prop()
  name?: string;

  // verified is a boolean value that indicates whether the user has verified their email address
  @ApiProperty({
    description: 'Indicates whether the user has verified their email address',
    example: true,
  })
  @Prop({
    type: MongooseSchema.Types.Boolean,
    default: false,
  })
  verified: boolean;

  // verificationCode is a 6-digit number that is sent to the user's email address to verify their email address
  @ApiHideProperty()
  @Prop({
    type: MongooseSchema.Types.Number,
  })
  verificationCode?: number;

  // verificationCodeExpiry is the date and time when the verification code expires
  @ApiHideProperty()
  @Prop({
    type: MongooseSchema.Types.Date,
  })
  verificationCodeExpiry?: Date;

  @ApiHideProperty()
  @Prop()
  resetToken?: string;

  // registerCode is used for when user is going to reset password or change password perform at time all same user login session will be logout
  @ApiHideProperty()
  @Prop({
    type: MongooseSchema.Types.Number,
  })
  registerCode?: number;

  @ApiProperty({
    description: 'Date of creation',
  })
  @Prop()
  createdAt?: Date;

  @ApiProperty({
    description: 'Date of last update',
  })
  @Prop()
  updatedAt?: Date;
}

export type UserIdentifier = Identifier | User;

export type UserDocument = HydratedDocument<User>;
export const UserSchema = SchemaFactory.createForClass(User);

UserSchema.index({ email: 1, isActive: 1 });
Enter fullscreen mode Exit fullscreen mode

The User Module in the NestJS Starter Kit is a comprehensive solution for managing user data and operations in a SaaS application. It integrates tightly with the Auth Module to provide a secure, scalable user management system that supports essential functionalities required in modern web applications. The use of Mongoose for data modeling, combined with the service and repository pattern, provides a clean and maintainable structure that is easy to extend and adapt to different use cases.

Workspace Module

The Workspace Module in the NestJS Starter Kit is designed to manage workspace-related functionalities within a SaaS application. This module leverages Mongoose for data modeling and provides services and repositories to perform various operations on the workspace data stored in MongoDB.

Overview of the Workspace Module

The Workspace Module is responsible for handling all operations related to workspaces. This includes creating new workspaces, retrieving workspace details, and managing workspace data within the application. The module integrates seamlessly with the rest of the application, providing necessary business logic and database interactions.

Key Components

  • WorkspaceModule: The main module that imports necessary components and provides services and repositories for workspace management.

    • MongooseModule.forFeature: Registers the Workspace schema with Mongoose, enabling MongoDB operations on the Workspace collection.
  • WorkspaceQueryService: Provides a service layer to interact with the WorkspaceRepository for querying workspace data. This service is responsible for:

    • Creating Workspaces: Manages the creation of new workspace records.
    • Finding Workspaces by ID: Retrieves workspace details using unique identifiers.
    • Error Handling: Ensures robust handling of exceptions during database operations.
  • WorkspaceRepository: Directly interacts with the MongoDB database using Mongoose to perform CRUD operations. It abstracts the database logic from the rest of the application, providing methods for:

    • Finding Workspaces: Both single and multiple workspace queries.
    • Creating Workspaces: Inserting new workspace records into the database.
    • Updating and Deleting Workspaces: Modifying or removing workspace records as needed.
  • WorkspaceSchema: Defines the schema for the Workspace collection in MongoDB using Mongoose. The schema includes fields like the workspace name, creation date, and update date.

    • Schema Decorators: Utilizes NestJS and Mongoose decorators to define schema properties and validation rules.
    • WorkspaceDocument: A type that represents a workspace document within the MongoDB collection.

How It Works

  1. Creating a Workspace: When a request is made to create a new workspace, the WorkspaceQueryService receives the request and passes it to the WorkspaceRepository for creation in the database.

  2. Retrieving Workspace Details: The WorkspaceQueryService uses the WorkspaceRepository to fetch workspace details by ID or other filters, ensuring only relevant data is retrieved.

  3. Data Management and Integrity: The WorkspaceRepository handles all direct interactions with MongoDB, ensuring data integrity and consistency across all workspace operations.

  4. Error Handling: The module is equipped with robust error handling to manage database and operational errors, ensuring smooth and secure workspace data operations.

workspace.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { DatabaseCollectionNames } from '../../shared/enums/db.enum';
import { WorkspaceQueryService } from './workspace.query-service';
import { WorkspaceRepository } from './workspace.repository';
import { WorkspaceSchema } from './workspace.schema';

@Module({
  imports: [MongooseModule.forFeature([{ name: DatabaseCollectionNames.WORKSPACE, schema: WorkspaceSchema }])],
  providers: [WorkspaceQueryService, WorkspaceRepository],
  exports: [WorkspaceQueryService],
})
export class WorkspaceModule {}
Enter fullscreen mode Exit fullscreen mode

workspace.query-service.ts

import { Injectable } from '@nestjs/common';

import { InternalServerErrorException } from '../../exceptions/internal-server-error.exception';

import { Workspace } from './workspace.schema';
import { WorkspaceRepository } from './workspace.repository';

@Injectable()
export class WorkspaceQueryService {
  constructor(private readonly workspaceRepository: WorkspaceRepository) {}

  async create(workspace: Workspace): Promise<Workspace> {
    try {
      return await this.workspaceRepository.create(workspace);
    } catch (error) {
      throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error);
    }
  }

  async findById(workspaceId: string): Promise<Workspace> {
    try {
      return await this.workspaceRepository.findById(workspaceId);
    } catch (error) {
      throw InternalServerErrorException.INTERNAL_SERVER_ERROR(error);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

workspace.repository.ts

import { FilterQuery, Model, ProjectionType, QueryOptions, UpdateQuery } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Injectable } from '@nestjs/common';

import { DatabaseCollectionNames } from '../../shared/enums/db.enum';
import { Workspace, WorkspaceDocument } from './workspace.schema';

@Injectable()
export class WorkspaceRepository {
  constructor(@InjectModel(DatabaseCollectionNames.WORKSPACE) private workspaceModel: Model<WorkspaceDocument>) {}

  async find(filter: FilterQuery<WorkspaceDocument>, selectOptions?: ProjectionType<WorkspaceDocument>): Promise<Workspace[]> {
    return this.workspaceModel.find(filter, selectOptions).lean();
  }

  async findOne(filter: FilterQuery<WorkspaceDocument>): Promise<Workspace> {
    return this.workspaceModel.findOne(filter).lean();
  }

  async create(workspace: Workspace): Promise<Workspace> {
    return this.workspaceModel.create(workspace);
  }

  async findById(workspaceId: string): Promise<Workspace> {
    return this.workspaceModel.findById(workspaceId).lean();
  }

  async findOneAndUpdate(
    filter: FilterQuery<WorkspaceDocument>,
    update: UpdateQuery<Workspace>,
    options?: QueryOptions<Workspace>,
  ): Promise<WorkspaceDocument> {
    return this.workspaceModel.findOneAndUpdate(filter, update, options).lean();
  }

  async findByIdAndDelete(workspaceId: string): Promise<Workspace> {
    return this.workspaceModel.findByIdAndDelete(workspaceId).lean();
  }
}
Enter fullscreen mode Exit fullscreen mode

workspace.schema.ts

import { ApiProperty } from '@nestjs/swagger';
import { HydratedDocument, Schema as MongooseSchema, Types } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';

import { DatabaseCollectionNames } from '../../shared/enums/db.enum';
import { Identifier } from '../../shared/types/schema.type';

@Schema({
  timestamps: true,
  collection: DatabaseCollectionNames.WORKSPACE,
})
export class Workspace {
  @ApiProperty({
    description: 'The unique identifier of the workspace',
    example: '507f191e810c19729de860ea',
  })
  @Prop({
    type: MongooseSchema.Types.ObjectId,
    default: () => new Types.ObjectId(),
  })
  _id?: Types.ObjectId;

  @ApiProperty({
    description: 'The name of the workspace',
    example: 'My Workspace',
  })
  @Prop({
    type: MongooseSchema.Types.String,
    required: true,
  })
  name: string;

  @ApiProperty({
    description: 'Date of creation',
  })
  @Prop()
  createdAt?: Date;

  @ApiProperty({
    description: 'Date of last update',
  })
  @Prop()
  updatedAt?: Date;
}

export type WorkspaceIdentifier = Identifier | Workspace;

export type WorkspaceDocument = HydratedDocument<Workspace>;
export const WorkspaceSchema = SchemaFactory.createForClass(Workspace);
Enter fullscreen mode Exit fullscreen mode

The Workspace Module in the NestJS Starter Kit provides a comprehensive solution for managing workspace data and operations in a SaaS application. It integrates tightly with other modules to offer a scalable and secure workspace management system that supports essential functionalities required in modern web applications. By utilizing Mongoose for data modeling and adopting the service and repository pattern, the module ensures a clean, maintainable structure that is easy to extend and adapt to various use cases.

Conclusion

The NestJS Starter Kit provides a powerful foundation for building scalable and secure SaaS applications. By integrating essential modules such as Auth, User, and Workspace, this starter kit streamlines the development process, enabling developers to focus on building features rather than managing complex infrastructure setup.

Key Benefits:

  • Scalable Architecture: The modular design supports scalability, allowing developers to easily extend and customize the application as it grows.
  • Security First: With JWT-based authentication and role-based access control, the starter kit ensures robust security practices are in place from the start.
  • Database Integration: Leveraging Mongoose for MongoDB, the starter kit simplifies data modeling and schema management, providing a strong foundation for managing application data.
  • Developer Friendly: Clear documentation and a well-structured codebase make it easy for developers to get up and running quickly, reducing the learning curve and speeding up the development cycle.

By providing pre-configured modules and a robust architecture, the NestJS Starter Kit helps developers jump-start their SaaS application development with best practices in mind. Whether you're building a small startup project or a large enterprise application, this starter kit offers the flexibility and security needed to succeed in today's fast-paced development environment.

We hope this guide helps you get started quickly and efficiently. We welcome contributions and feedback to improve the starter kit and make it even more useful for the developer community.

Happy coding!


For more information and to get started, check out the GitHub repository and the detailed setup guide on dev.to.

Feel free to contribute or raise issues to improve this project further!

Top comments (0)