Entities and Repositories
Move the cats into a database: connect with TypeOrmModule, turn the Cat class into an entity, register it so a repository can be injected, and write the service against find, save, delete and the query builder.
What you'll learn
- Connect an application with TypeOrmModule.forRoot() and explain synchronize and autoLoadEntities
- Define an entity with @Entity, @PrimaryGeneratedColumn and @Column, and register it with forFeature so its repository is injectable
- Use a repository's find, findOneBy, save and delete, decide where a 404 comes from, and write an aggregate with the query builder
Every cat so far has lived in an array inside CatsService, which means every cat dies with the process. A real service keeps its data in a database, and the question this lesson answers is how Nest talks to one without the database leaking into every controller and service. The docs' answer is TypeORM: an object-relational mapper that turns a class into a table, and gives each entity a repository with find, save and delete so the service works with objects and the mapper writes the SQL.
Connecting
@nestjs/typeorm wraps TypeORM in a module. The root module opens the connection:
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
],
})
export class AppModule {}
The type selects the driver; the docs use MySQL, and this runtime uses 'sqljs', SQLite compiled to WebAssembly, which needs no host, port or credentials and keeps the database in memory for the length of a run. Everything else in this chapter is identical whichever driver you pick, which is the point of an ORM. synchronize: true creates or alters tables to match the entities at every boot: perfect for development and a lesson, dangerous in production, where a renamed property would drop a column; there, migrations replace it. autoLoadEntities: true spares you the entities list: every entity a feature module registers is included.
Two options you will meet in the wild: retryAttempts and retryDelay retry a failing connection before giving up, and forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config) => ({ ... }) }) builds the same options from configuration, as lesson 1 taught.
Entities
An entity is a class whose instances are rows:
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column({ default: true })
isActive: boolean;
}
@Entity() names the table after the class. @PrimaryGeneratedColumn() is an auto-incremented primary key; @Column() maps a property to a column whose type is inferred from the TypeScript type, with options for default, unique, nullable, length and the rest. What is not decorated is not a column: a plain property stays in memory only.
Repositories
A feature module registers the entities it works with, and that registration is what makes their repositories injectable:
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
})
export class UsersModule {}
@Injectable()
export class UsersService {
constructor(@InjectRepository(User) private readonly users: Repository<User>) {}
findAll(): Promise<User[]> {
return this.users.find();
}
findOne(id: number): Promise<User | null> {
return this.users.findOneBy({ id });
}
async remove(id: number): Promise<void> {
await this.users.delete(id);
}
}
@InjectRepository(User) names the token, because Repository<User> is generic and the type alone does not say which entity. Then: create() builds an instance without saving it, save() inserts or updates and returns the entity with its generated id, find() takes where, order, relations and take, findOneBy() returns the row or null, delete() returns a result whose affected says how many rows went. Nothing is a 404 until your service decides it is.
For a query that is not a lookup, the query builder writes SQL by method calls and returns raw rows:
this.users.createQueryBuilder('user').select('COUNT(*)', 'count').getRawOne();
Your task
The cats move into the database.
- In
AppModule, connect withTypeOrmModule.forRoot(): typesqljs, entities loaded automatically, schema synchronised. cats/cat.entity.tsbecomes an entity: a generated primary key,name,ageandadoptablecolumns, the last defaulting totrue.CatsModuleregisters the entity, so that theRepository<Cat>the service already asks for exists.- In
CatsService,findOnelooks a cat up and turnsnullinto a404;removeturns "nothing deleted" into the same404;statscounts the cats and averages their ages in one query with the query builder.
Run before touching anything: the service asks for a repository nobody provides, and Nest tells you so, with the token's name.
When it fails
Nest can't resolve dependencies of the CatsService (?), argumentCatRepository:forFeature([Cat])is missing fromCatsModule, orforRootis missing altogether.Entity metadata for Cat was not foundorNo metadata for "Cat" was found: the class has no@Entity(), or it was not registered andautoLoadEntitiesis off.Entity "Cat" does not have a primary column:@PrimaryGeneratedColumn()is missing; every entity needs a primary key.adoptableis missing from every cat: the property has no@Column(), so TypeORM does not know it is a column.DELETE /cats/9answers204:delete()does not throw for a missing row; checkresult.affected.
Remember
TypeOrmModule.forRoot()connects once;type: 'sqljs'here, the same code with any driver.@Entity(),@PrimaryGeneratedColumn()and@Column()turn a class into a table;synchronizebuilds it, in development only.forFeature([Entity])in a module makes@InjectRepository(Entity)work in that module.- A repository never throws for a missing row:
findOneByreturnsnull,deletereturnsaffected.
Stuck? Show a hint
app.module.ts: TypeOrmModule.forRoot({ type: 'sqljs', autoLoadEntities: true, synchronize: true }). cat.entity.ts: @Entity() on the class, @PrimaryGeneratedColumn() on id, @Column() on name and age, @Column({ default: true }) on adoptable. cats.module.ts: imports: [TypeOrmModule.forFeature([Cat])]. Service: findOneBy({ id }) and throw when null; delete(id) and throw when result.affected is 0; createQueryBuilder('cat').select('COUNT(*)', 'count').addSelect('AVG(cat.age)', 'averageAge').getRawOne().