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

feat: add typegoose recipe #2572

Open
wants to merge 4 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
224 changes: 224 additions & 0 deletions content/recipes/typegoose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
### Typegoose (Mongoose)

[Typegoose](https://github.com/typegoose/typegoose) is a well maintained TypeScript wrapper for the [Mongoose ODM](https://mongoosejs.com/) library and is similar in nature to [the Mongoose Module](/techniques/mongodb) Nest provides. However, Typegoose's TypeScript support is more expansive, making your entity/model classes and your work with Mongoose, and in turn MongoDB, more expressive. Typegoose also offers extras like a specialized logger, a few extra types for a much better and more concise usage of Typegoose (and Mongoose), some added type guard methods, a number of specialized functions and more.

This recipe will get you started working with Typegoose in NestJS.

#### Getting started
smolinari marked this conversation as resolved.
Show resolved Hide resolved

To begin, you'll need to install the following dependencies into your [already created NestJS app](/first-steps):

```typescript
$ npm install --save mongoose @typegoose/typegoose @m8a/nestjs-typegoose
Copy link
Member

Choose a reason for hiding this comment

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

Are we sure we want to link a library that has ~500 weekly downloads in the docs 🤔 ?

Copy link
Contributor Author

@smolinari smolinari Feb 3, 2023

Choose a reason for hiding this comment

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

@kamilmysliwiec - It's a chicken and egg dilemma. 😁

The @typegoose/typegoose package has 79k downloads a week. That's the popularity we'd want to possibly hook into. And vice-versa of course.

Typegoose is a very good alternative for using Mongoose. The Typegoose package is very well maintained and documented and I take care of the nestjs-typegoose module updates now too.

If more people see the recipe, the more the module will be downloaded. 😉

We've also been promoting Typegoose (mostly me) in the Discord server and the database forum on Discord has had the tag "mongoose/ typegoose" for several months now.

And, if I may be so bold, the Nest team could theoretically take over the nestjs-typegoose module and drop the Mongoose module altogether (sunset it). It would be somewhat less effort from a support standpoint (although I'd still do the support in Discord) because Typegoose's maintainer has his own Discord server. The Typegoose docs are more detailed too. 😀 Just an idea.....

Scott

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Theoretically this recipe could replace the Mongoose recipe too. It's outdated.

https://docs.nestjs.com/recipes/mongodb

Scott

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trying to reinvigorate the decision to get this merged, the actual popularity we'd want to be jumping on is the 2 million downloads a week for Mongoose. Typegoose is a better (I'd say the best) TypeScript wrapper for Mongoose. The developer of Typegoose is really sharp and he even helps fix Mongoose TypeScript issues, when they pop up. The package can help promote Mongoose usage with Nest for sure! 😀

Scott

Copy link
Contributor Author

@smolinari smolinari Nov 11, 2023

Choose a reason for hiding this comment

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

I'd also like to put in that I've been helping in the forum (Discord) a lot and I'm also willing to help continue to support this package, if it became THE official suggested module for using Mongoose with Nest. That means, I'd suggest even taking down the Nest mongoose module. That would save you all time and effort, by putting the support on the community and get Nest users something they can be very successful with using Mongoose with Nest!

Scott

```
> info **info** [Typegoose](https://github.com/typegoose/typegoose) and the [Typegoose module](https://github.com/m8a-io/m8a) are third party packages and are not managed by the entirety of the NestJS core team. Please, report any issues found with either library in their respective repositories (linked above).

Here is a short description of each package.

- **mongoose** - the core and powerful Mongoose ODM library
- **@typegoose/typegoose** - the Typegoose library
- **@m8a/nestjs-typegoose** - the package containing the Typegoose module for plugging in Typegoose into Nest

> info **Hint** Some of the content in this recipe was taken from the [documentation website of the @m8a/nestjs-typegoose package](https://nestjs-typegoose.m8a.io/). You can get further details there about the Typegoose module and can also get further details about Typegoose at their [docs website](https://typegoose.github.io/typegoose/docs/guides/quick-start-guide).

#### Setting up the DB Connection
For the next step, we will need to configure the connection to the MongoDB database. To do that, we'll use the `TypegooseModule.forRoot` static method.

```typescript
// app.module.ts

import { Module } from "@nestjs/common";
import { TypegooseModule } from "@m8a/nestjs-typegoose";

@Module({
imports: [
TypegooseModule.forRoot("mongodb://localhost:27017/otherdb", {
// other connection options
}),
CatsModule
]
})
export class ApplicationModule {}
```
The second parameter of the `TypegooseModule.forRoot` static method entails [the additional connection options](https://mongoosejs.com/docs/connections.html#options) available from Mongoose.

Also notice we imported the `CatsModule`. We will create that module shortly.

If you have requirements to use multiple databases, you can also implement [multiple connections to different databases](https://nestjs-typegoose.m8a.io/docs/multiple-connections).

#### Creating Entities

Now that you have the dependencies installed and the database connection is set up, let's create our first entity. This is a very simplified example.

```typescript
// cat.entity.ts

import { Prop } from "@typegoose/typegoose";

export class Cat {
@Prop({ required: true })
public name!: string;
smolinari marked this conversation as resolved.
Show resolved Hide resolved
}
```

Entity classes, like above, are basically schema definitions, which Typegoose converts to models in Mongoose.

> info **Hint** You can also call your entity file "model" i.e. `cat.model.ts`. This file naming convention is up to you.

#### Creating a Service (with a Model)

In order to inject a Mongoose model into any Nest provider, you need to use the `@InjectModel` decorator inside your provider class' constructor as shown in the following example of a service.

```typescript
// cat.service.ts

import { Injectable } from "@nestjs/common";
import { InjectModel } from "@m8a/nestjs-typegoose";
import { Cat } from "./cat.model";
import { ReturnModelType } from "@typegoose/typegoose";

@Injectable()
export class CatsService {
constructor(
@InjectModel(Cat) private readonly catModel: ReturnModelType<typeof Cat>
) {}

async create(createCatDto: { name: string }): Promise<Cat> {
const createdCat = new this.catModel(createCatDto);
return await createdCat.save();
}

async findAll(): Promise<Cat[] | null> {
return await this.catModel.find().exec();
}
}
```
From the example above, you can see a more specific type used by Typegoose to define the model called (`ReturnModelType`). This type is more expressive than the `HydratedDocument` type provided by Mongoose.

#### Providing the Model

We have to make sure we provide the needed models to our service with `TypegooseModule.forFeature` for the `@InjectModel` to work. This helps prevents unauthorized access to other models.

```typescript
// cat.module.ts
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@m8a/nestjs-typegoose";
import { Cat } from "./cat.model";
import { ReturnModelType } from "@typegoose/typegoose";

@Injectable()
export class CatsService {
constructor(
@InjectModel(Cat) private readonly catModel: ReturnModelType<typeof Cat>
) {}

async create(createCatDto: { name: string }): Promise<Cat> {
const createdCat = new this.catModel(createCatDto);
return await createdCat.save();
}

async findAll(): Promise<Cat[] | null> {
return await this.catModel.find().exec();
}
}
```
#### Async Configuration

To provide asynchronous mongoose schema options (similar to NestJS' Mongoose module implementation) you can use the `TypegooseModule.forRootAsync`

```typescript
// cat.module.ts

@Module({
imports: [
TypegooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
uri: configService.getString("MONGODB_URI")
// ...typegooseOptions (Note: config is spread with the uri)
}),
inject: [ConfigService]
})
]
})
export class CatsModule {}
```
The `typegooseOptions` is spread with the `uri`. The `uri` is required!

You can also use a class with `useClass`.
```typescript
// cat.module.ts

import {
TypegooseOptionsFactory,
TypegooseModuleOptions
} from "nestjs-typegoose";

class TypegooseConfigService extends TypegooseOptionsFactory {
createTypegooseOptions():
| Promise<TypegooseModuleOptions>
| TypegooseModuleOptions {
return {
uri: "mongodb://localhost/nest"
};
}
}

@Module({
imports: [
TypegooseModule.forRootAsync({
useClass: TypegooseConfigService
})
]
})
export class CatsModule {}
```
Or, if you want to prevent creating another `TypegooseConfigService` class and want to use it from another imported module then use `useExisting`.
```typescript
// cat.module.ts

@Module({
imports: [
TypegooseModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService
})
]
})
export class CatsModule {}
```

#### Testing
Like Nest's Mongoose module (see the [testing section](http://localhost:4200/techniques/mongodb#testing)), nestjs-typegoose's `forFeature` and `forRoot` rely on a database connection to work. To unit test your `CatService` without connecting to a MongoDB database, you'll need to create a fake model using a [custom provider](/fundamentals/custom-providers).
```typescript
// cat.module.ts

import { getModelToken } from "@m8a/nestjs-typegoose";

@Module({
CatService,
{
provide: getModelToken('Cat'),
useValue: fakeCatModel
}
})
```
In a spec file this would look like:
```typescript
// cat.service.spec.ts

const fakeCatModel = jest.fn();

const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getModelToken(Cat.name),
useValue: fakeCatModel
},
CatService
]
}).compile();
```
Overall, between the docs of the Typegoose module and Typegoose, you can attain a strong basis to work with Mongoose and NestJS in a much more powerful and typed manner.

Should you have any further questions, you can ask them on the [NestJS Discord channel](https://discord.gg/nestjs). Should you find issues with the [Typegoose](https://github.com/typegoose/typegoose) or the [Typegoose module](https://github.com/m8a-io/m8a) packages, please report them to their respective repositories, as the Nest team do not maintain these libraries.
1 change: 1 addition & 0 deletions src/app/homepage/menu/menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export class MenuComponent implements OnInit {
{ title: 'MikroORM', path: '/recipes/mikroorm' },
{ title: 'TypeORM', path: '/recipes/sql-typeorm' },
{ title: 'Mongoose', path: '/recipes/mongodb' },
{ title: 'Typegoose', path: '/recipes/typegoose' },
{ title: 'Sequelize', path: '/recipes/sql-sequelize' },
{ title: 'Router module', path: '/recipes/router-module' },
{ title: 'Swagger', path: '/recipes/swagger' },
Expand Down
7 changes: 7 additions & 0 deletions src/app/homepage/pages/recipes/recipes.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DocumentationComponent } from './documentation/documentation.component'
import { HotReloadComponent } from './hot-reload/hot-reload.component';
import { MikroOrmComponent } from './mikroorm/mikroorm.component';
import { MongodbComponent } from './mongodb/mongodb.component';
import { TypegooseComponent } from './typegoose/typegoose.component';
import { PrismaComponent } from './prisma/prisma.component';
import { ReplComponent } from './repl/repl.component';
import { ServeStaticComponent } from './serve-static/serve-static.component';
Expand Down Expand Up @@ -100,6 +101,11 @@ const routes: Routes = [
component: ReplComponent,
data: { title: 'REPL' },
},
{
path: 'typegoose',
component: TypegooseComponent,
data: { title: 'Typegoose (Mongoose)' },
},
];

@NgModule({
Expand All @@ -119,6 +125,7 @@ const routes: Routes = [
ServeStaticComponent,
NestCommanderComponent,
ReplComponent,
TypegooseComponent
],
})
export class RecipesModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { BasePageComponent } from '../../page/page.component';

@Component({
selector: 'app-typegoose',
templateUrl: './typegoose.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TypegooseComponent extends BasePageComponent {}