1. Nest框架与MySQL数据库入门指南作为现代Node.js后端开发的主流框架NestJS凭借其模块化设计和TypeScript支持已成为企业级应用开发的热门选择。而MySQL作为最流行的关系型数据库之一与Nest的整合是每个全栈开发者必须掌握的技能。本文将带你从零开始搭建Nest项目并实现完整的MySQL数据库操作流程。提示本文基于Nest 9.x和MySQL 8.0环境所有代码示例均通过TypeScript 4.7验证1.1 环境准备与项目初始化首先确保你的开发环境已安装Node.js 16推荐18.x LTS版本MySQL 8.0社区版即可任意代码编辑器VS Code为首选通过以下命令创建新项目npm i -g nestjs/cli nest new nest-mysql-demo cd nest-mysql-demo安装MySQL驱动和TypeORMNest官方推荐的ORMnpm install nestjs/typeorm typeorm mysql21.2 数据库连接配置在app.module.ts中配置TypeORM连接import { TypeOrmModule } from nestjs/typeorm; Module({ imports: [ TypeOrmModule.forRoot({ type: mysql, host: localhost, port: 3306, username: root, password: your_password, database: nest_demo, entities: [__dirname /**/*.entity{.ts,.js}], synchronize: true, // 开发环境可用生产环境必须关闭 }), ], }) export class AppModule {}警告synchronize: true会基于实体类自动同步数据库结构方便开发但可能造成生产环境数据丢失务必在正式部署时设为false2. 实体定义与CRUD实现2.1 创建第一个实体假设我们要开发用户管理系统先创建用户实体// src/users/user.entity.ts import { Entity, PrimaryGeneratedColumn, Column } from typeorm; Entity() export class User { PrimaryGeneratedColumn() id: number; Column({ length: 50 }) username: string; Column({ select: false }) // 查询时默认不返回密码 password: string; Column({ default: true }) isActive: boolean; Column({ type: timestamp, default: () CURRENT_TIMESTAMP }) createdAt: Date; }2.2 实现服务层和数据访问创建用户模块和服务nest generate module users nest generate service users完善服务逻辑// src/users/users.service.ts import { Injectable } from nestjs/common; import { InjectRepository } from nestjs/typeorm; import { Repository } from typeorm; import { User } from ./user.entity; Injectable() export class UsersService { constructor( InjectRepository(User) private usersRepository: RepositoryUser, ) {} async create(user: PartialUser): PromiseUser { return this.usersRepository.save(user); } async findAll(): PromiseUser[] { return this.usersRepository.find(); } async findOne(id: number): PromiseUser { return this.usersRepository.findOne({ where: { id } }); } async update(id: number, updateData: PartialUser): Promisevoid { await this.usersRepository.update(id, updateData); } async remove(id: number): Promisevoid { await this.usersRepository.delete(id); } }2.3 控制器层实现生成控制器并实现RESTful接口nest generate controller users// src/users/users.controller.ts import { Controller, Get, Post, Body, Param, Put, Delete } from nestjs/common; import { UsersService } from ./users.service; import { User } from ./user.entity; Controller(users) export class UsersController { constructor(private readonly usersService: UsersService) {} Post() create(Body() user: User) { return this.usersService.create(user); } Get() findAll() { return this.usersService.findAll(); } Get(:id) findOne(Param(id) id: string) { return this.usersService.findOne(id); } Put(:id) update(Param(id) id: string, Body() updateData: PartialUser) { return this.usersService.update(id, updateData); } Delete(:id) remove(Param(id) id: string) { return this.usersService.remove(id); } }3. 高级查询与事务处理3.1 复杂查询构建TypeORM提供了强大的查询构建器以下是几个实用示例// 在UsersService中添加方法 async findActiveUsers(): PromiseUser[] { return this.usersRepository .createQueryBuilder(user) .where(user.isActive :isActive, { isActive: true }) .orderBy(user.createdAt, DESC) .getMany(); } async searchUsers(keyword: string): PromiseUser[] { return this.usersRepository .createQueryBuilder() .where(username LIKE :keyword, { keyword: %${keyword}% }) .orWhere(email LIKE :keyword, { keyword: %${keyword}% }) .getMany(); }3.2 事务处理实战对于需要原子性操作的多表写入必须使用事务async transferCredits( fromId: number, toId: number, amount: number, ): Promisevoid { await this.usersRepository.manager.transaction(async (manager) { const fromUser await manager.findOne(User, { where: { id: fromId } }); const toUser await manager.findOne(User, { where: { id: toId } }); if (!fromUser || !toUser) { throw new Error(用户不存在); } if (fromUser.credits amount) { throw new Error(余额不足); } fromUser.credits - amount; toUser.credits amount; await manager.save(User, fromUser); await manager.save(User, toUser); }); }4. 性能优化与生产环境实践4.1 连接池配置优化在高并发场景下合理的连接池配置至关重要TypeOrmModule.forRoot({ // ...其他配置 extra: { connectionLimit: 10, // 连接池大小 waitForConnections: true, queueLimit: 0, }, }),推荐配置规则开发环境5-10个连接生产环境按 (核心数 * 2) 有效磁盘数 计算4.2 索引与查询优化为常用查询字段添加索引Entity() export class User { // ... Column({ length: 50 }) Index(IDX_USERNAME, { unique: true }) username: string; Column() Index(IDX_EMAIL, { unique: true }) email: string; }查询优化技巧避免SELECT *只查询必要字段对分页查询使用skip()和take()替代limit和offset复杂查询考虑使用原生SQL4.3 生产环境部署要点必须关闭synchronize使用迁移管理数据库变更启用SSL连接云数据库必备配置适当的连接超时时间实现健康检查端点监控数据库连接状态// 健康检查示例 Get(health) async healthCheck() { try { await this.usersRepository.query(SELECT 1); return { status: ok }; } catch (error) { throw new HttpException( { status: down, error: error.message }, HttpStatus.SERVICE_UNAVAILABLE, ); } }5. 常见问题排查5.1 连接问题速查表错误现象可能原因解决方案ECONNREFUSED数据库服务未启动检查MySQL服务状态ER_ACCESS_DENIED_ERROR认证失败验证用户名/密码ER_BAD_DB_ERROR数据库不存在创建指定数据库ETIMEDOUT网络问题检查防火墙/安全组规则5.2 性能问题诊断慢查询日志分析-- 在MySQL中启用慢查询日志 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;使用EXPLAIN分析查询计划const query this.usersRepository .createQueryBuilder(user) .where(user.isActive :isActive, { isActive: true }) .getSql(); const explainResult await this.usersRepository.query( EXPLAIN ${query}, );5.3 事务隔离级别问题MySQL默认使用REPEATABLE READ隔离级别可能导致幻读。对于需要更高隔离级别的操作async sensitiveOperation() { await this.usersRepository.manager.transaction( SERIALIZABLE, async (manager) { // 事务操作 }, ); }6. 扩展实践与进阶技巧6.1 多数据库连接配置对于需要连接多个MySQL实例的场景Module({ imports: [ TypeOrmModule.forRoot({ name: primary, type: mysql, // 主数据库配置 }), TypeOrmModule.forRoot({ name: secondary, type: mysql, // 从数据库配置 }), ], }) export class AppModule {} // 使用时指定连接 InjectRepository(User, secondary) private secondaryUserRepository: RepositoryUser6.2 数据迁移管理使用TypeORM迁移工具管理数据库变更安装typeorm扩展npm install ts-node typeorm -D创建迁移npx typeorm migration:create src/migrations/InitSchema编写迁移文件import { MigrationInterface, QueryRunner } from typeorm; export class InitSchema implements MigrationInterface { public async up(queryRunner: QueryRunner): Promisevoid { await queryRunner.query( CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, isActive BOOLEAN DEFAULT TRUE, createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ); } public async down(queryRunner: QueryRunner): Promisevoid { await queryRunner.query(DROP TABLE users); } }运行迁移npx typeorm migration:run6.3 软删除实现通过装饰器轻松实现软删除import { DeleteDateColumn } from typeorm; Entity() export class User { // ... DeleteDateColumn() deletedAt?: Date; } // 查询时自动过滤已删除记录 this.usersRepository.find(); // 需要查询已删除记录时 this.usersRepository.find({ withDeleted: true });7. 安全最佳实践7.1 敏感数据保护密码存储必须加密import * as bcrypt from bcrypt; async create(user: PartialUser): PromiseUser { const salt await bcrypt.genSalt(); user.password await bcrypt.hash(user.password, salt); return this.usersRepository.save(user); }使用select: false隐藏敏感字段实现DTO层过滤返回数据7.2 SQL注入防护TypeORM已内置防护措施但需注意始终使用参数化查询避免直接拼接SQL对用户输入进行严格验证// 安全做法 this.usersRepository .createQueryBuilder() .where(username :username, { username: userInput }); // 危险做法绝对避免 this.usersRepository.query( SELECT * FROM users WHERE username ${userInput}, );7.3 连接安全配置生产环境必须启用SSLTypeOrmModule.forRoot({ ssl: { rejectUnauthorized: true, ca: fs.readFileSync(/path/to/server-ca.pem).toString(), cert: fs.readFileSync(/path/to/client-cert.pem).toString(), key: fs.readFileSync(/path/to/client-key.pem).toString(), }, }),8. 测试策略8.1 单元测试示例使用Jest测试服务层describe(UsersService, () { let service: UsersService; let repository: RepositoryUser; beforeEach(async () { const module: TestingModule await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(User), useClass: Repository, }, ], }).compile(); service module.getUsersService(UsersService); repository module.getRepositoryUser(getRepositoryToken(User)); }); it(should create a user, async () { const userData { username: test, password: 123456 }; jest.spyOn(repository, save).mockResolvedValueOnce({ id: 1, ...userData }); const result await service.create(userData); expect(result).toHaveProperty(id); expect(repository.save).toHaveBeenCalled(); }); });8.2 E2E测试配置测试模块配置使用测试数据库Module({ imports: [ TypeOrmModule.forRoot({ type: mysql, host: localhost, port: 3307, // 测试数据库端口 username: test, password: test, database: test_db, entities: [__dirname /**/*.entity{.ts,.js}], synchronize: true, // 测试环境可以开启 }), UsersModule, ], }) export class TestModule {}9. 性能监控与日志9.1 慢查询监控集成TypeORM日志记录慢查询TypeOrmModule.forRoot({ logging: [query], maxQueryExecutionTime: 1000, // 超过1秒的查询记录为慢查询 }),9.2 自定义查询日志通过订阅TypeORM事件实现高级日志import { Connection, QueryRunner } from typeorm; Injectable() export class DatabaseLogger implements Logger { constructor(private connection: Connection) { this.connection.subscribe((event) { if (event.queryRunner?.data?.queryDuration 500) { console.warn(Slow query detected: ${event.queryRunner.query}); } }); } }10. 项目结构优化建议10.1 模块化组织推荐的项目结构src/ ├── modules/ │ ├── users/ │ │ ├── user.entity.ts │ │ ├── users.service.ts │ │ ├── users.controller.ts │ │ ├── users.module.ts │ │ └── dto/ │ │ ├── create-user.dto.ts │ │ └── update-user.dto.ts ├── shared/ │ ├── database/ │ │ └── database.module.ts │ └── utils/ └── app.module.ts10.2 数据库连接集中管理创建共享的数据库模块// src/shared/database/database.module.ts Module({ imports: [ TypeOrmModule.forRootAsync({ useFactory: () ({ type: mysql, host: process.env.DB_HOST, // ...其他配置 }), }), ], exports: [TypeOrmModule], }) export class DatabaseModule {}11. 实际项目经验分享在大型项目中处理千万级用户表时我们发现几个关键点分库分表策略当单表超过500万行时考虑按用户ID哈希分片读写分离配置多个从库分担读压力缓存层对热点数据使用Redis缓存连接管理使用连接中间件如ProxySQL管理连接池一个实用的连接池监控代码片段setInterval(async () { const pool this.connection.driver.pool; console.log({ total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, }); }, 5000);12. 调试技巧12.1 查询日志输出临时开启完整SQL日志TypeOrmModule.forRoot({ logger: advanced-console, logging: [query, error], }),12.2 事务调试在事务中插入调试点await this.usersRepository.manager.transaction(async (manager) { console.time(transaction); // 业务代码 console.timeEnd(transaction); });13. 版本兼容性说明不同版本组合的注意事项NestJSTypeORMMySQL驱动注意事项9.x0.3.xmysql2 2.3推荐组合8.x0.2.xmysql2 2.2需要手动安装nestjs/typeorm7.x0.2.xmysql 2.18不支持async/await语法14. 替代方案比较14.1 Sequelize vs TypeORM特性TypeORMSequelize语法风格装饰器ActiveRecord传统ORM接口TypeScript支持原生优秀需要额外类型包关联查询更直观稍显复杂社区活跃度高极高迁移工具内置需要单独安装14.2 原生MySQL驱动对于简单场景可以直接使用mysql2包import { createPool } from mysql2/promise; const pool createPool({ host: localhost, user: root, database: test, waitForConnections: true, connectionLimit: 10, }); async function queryUsers() { const [rows] await pool.query(SELECT * FROM users LIMIT 10); return rows; }15. 资源推荐官方文档NestJS TypeORM集成TypeORM MySQL配置性能优化指南MySQL官方优化白皮书《高性能MySQL》第3版监控工具Percona Monitoring and ManagementPrometheus Grafana MySQL仪表板安全审计工具sqlmap用于检测注入漏洞MySQL Enterprise Audit16. 升级与迁移策略从旧版本升级时的注意事项备份优先执行任何升级前完整备份数据库逐步升级先升级开发环境验证后再升级生产变更日志仔细阅读TypeORM和Nest的CHANGELOG测试覆盖确保有足够的测试用例验证关键功能典型升级步骤# 1. 备份package.json # 2. 更新依赖 npm install nestjs/typeormlatest typeormlatest mysql2latest # 3. 运行测试 npm test # 4. 检查弃用警告17. 云数据库实践连接云数据库如AWS RDS的特殊配置TypeOrmModule.forRoot({ type: mysql, host: process.env.RDS_HOSTNAME, port: parseInt(process.env.RDS_PORT), username: process.env.RDS_USERNAME, password: process.env.RDS_PASSWORD, database: process.env.RDS_DB_NAME, ssl: { rejectUnauthorized: true, ca: fs.readFileSync(/path/to/rds-combined-ca-bundle.pem).toString(), }, }),云环境最佳实践使用IAM数据库认证替代密码配置自动扩展的连接池启用数据库代理减少连接开销设置适当的自动备份策略18. 连接问题深度排查当遇到间歇性连接断开时可按以下步骤排查检查MySQL服务器端的wait_timeout设置验证TCP keepalive配置检查网络设备负载均衡器、防火墙的超时设置在TypeORM中配置连接验证TypeOrmModule.forRoot({ // ... extra: { connectionTimeout: 30000, idleTimeout: 10000, keepAliveInitialDelay: 10000, }, }),19. 批量操作优化处理大量数据时的性能技巧使用批量插入代替循环插入async bulkCreate(users: User[]) { await this.usersRepository .createQueryBuilder() .insert() .into(User) .values(users) .execute(); }分批次处理大数据集const batchSize 1000; for (let i 0; i total; i batchSize) { const batch data.slice(i, i batchSize); await this.bulkCreate(batch); }禁用日志提升批量操作性能await this.usersRepository.manager.transaction(async (manager) { await manager.query(SET FOREIGN_KEY_CHECKS 0); await manager.query(SET UNIQUE_CHECKS 0); await manager.query(SET AUTOCOMMIT 0); // 批量操作 await manager.query(SET FOREIGN_KEY_CHECKS 1); await manager.query(SET UNIQUE_CHECKS 1); await manager.query(SET AUTOCOMMIT 1); });20. 数据类型最佳实践MySQL与TypeScript类型映射建议MySQL类型TypeORM装饰器TypeScript类型注意事项INTColumn(int)number主键推荐使用PrimaryGeneratedColumn()VARCHARColumn(varchar, { length: 255 })string根据实际需求设置长度TEXTColumn(text)string大文本内容使用BOOLEANColumn(boolean)booleanMySQL实际存储为TINYINT(1)DATETIMEColumn(datetime)Date推荐使用TIMESTAMP自动管理时间DECIMALColumn(decimal, { precision: 10, scale: 2 })string避免使用float处理精确计算JSONColumn(json)object自动序列化/反序列化21. 关联关系实战实现用户与文章的1对多关系// user.entity.ts OneToMany(() Article, (article) article.author) articles: Article[]; // article.entity.ts ManyToOne(() User, (user) user.articles) JoinColumn({ name: author_id }) author: User;查询关联数据的几种方式使用relations选项this.usersRepository.find({ relations: [articles] });使用查询构建器this.usersRepository .createQueryBuilder(user) .leftJoinAndSelect(user.articles, articles) .where(user.id :id, { id: 1 }) .getOne();使用RelationId获取外键RelationId((article: Article) article.author) authorId: number;22. 自定义仓储模式扩展默认Repository功能// src/users/users.repository.ts EntityRepository(User) export class UsersRepository extends RepositoryUser { async findActiveUsers(): PromiseUser[] { return this.find({ where: { isActive: true } }); } } // 在模块中注册 Module({ imports: [TypeOrmModule.forFeature([User, UsersRepository])], providers: [UsersService], controllers: [UsersController], }) export class UsersModule {} // 在服务中使用 Injectable() export class UsersService { constructor( InjectRepository(User) private usersRepository: UsersRepository, ) {} }23. 数据库健康检查实现全面的健康检查端点Get(health) async healthCheck() { const dbCheck await this.checkDatabase(); const redisCheck await this.checkRedis(); return { status: dbCheck redisCheck ? healthy : degraded, details: { database: dbCheck ? up : down, redis: redisCheck ? up : down, timestamp: new Date().toISOString(), }, }; } private async checkDatabase(): Promiseboolean { try { await this.usersRepository.query(SELECT 1); return true; } catch { return false; } }24. 连接池监控与管理高级连接池配置示例TypeOrmModule.forRoot({ // ...其他配置 extra: { poolSize: 20, acquireTimeout: 30000, idleTimeout: 10000, queueLimit: 0, enableKeepAlive: true, keepAliveInitialDelay: 10000, }, }),监控连接池状态的实用方法Injectable() export class DatabaseMonitorService { constructor(private connection: Connection) {} getPoolStats() { const pool this.connection.driver.pool; return { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, max: pool.options.connectionLimit, }; } }25. 生产环境部署清单上线前必须检查的项目安全配置禁用synchronize启用SSL连接使用强密码和最小权限账户性能配置优化连接池大小设置适当的查询超时配置慢查询日志监控配置实现健康检查端点设置数据库性能监控配置告警规则备份策略定期全量备份二进制日志增量备份测试恢复流程灾难恢复多可用区部署故障转移演练回滚方案准备26. 性能基准测试使用Jest进行简单的性能测试describe(Performance Tests, () { it(should handle 100 concurrent queries, async () { const start Date.now(); const promises Array(100) .fill(0) .map(() usersService.findAll()); await Promise.all(promises); const duration Date.now() - start; console.log(100 queries took ${duration}ms); expect(duration).toBeLessThan(1000); }); });关键性能指标参考值操作类型单次操作预期耗时并发能力简单查询10ms1000 QPS复杂联查50-100ms100 QPS插入操作20-50ms500 QPS更新操作30-60ms300 QPS27. 连接泄漏排查检测和修复连接泄漏的步骤监控连接数增长setInterval(() { const pool connection.driver.pool; console.log(Connections: ${pool.totalCount}/${pool.options.connectionLimit}); }, 1000);使用AsyncLocalStorage跟踪连接获取import { AsyncLocalStorage } from async_hooks; const connectionStorage new AsyncLocalStorage(); async function withConnectionT(fn: () PromiseT): PromiseT { return connectionStorage.run(new Date(), async () { try { return await fn(); } finally { const start connectionStorage.getStore(); if (start Date.now() - start 1000) { console.warn(Long running connection detected); } } }); }常见泄漏场景未处理异常的异步操作忘记释放的手动事务循环中未关闭的游标28. 多租户实现使用TypeORM实现多租户架构// 租户感知的Repository EntityRepository(User) export class TenantAwareRepository extends RepositoryUser { async findForTenant(tenantId: string): PromiseUser[] { return this.createQueryBuilder(user) .where(user.tenantId :tenantId, { tenantId }) .getMany(); } } // 请求作用域的租户ID Injectable({ scope: Scope.REQUEST }) export class TenantService { tenantId: string; } // 在服务中使用 Injectable() export class UsersService { constructor( InjectRepository(User) private usersRepository: TenantAwareRepository, private tenantService: TenantService, ) {} async findAll(): PromiseUser[] { return this.usersRepository.findForTenant(this.tenantService.tenantId); } }29. 地理空间数据处理MySQL支持的地理空间功能示例Entity() export class Place { PrimaryGeneratedColumn() id: number; Column(point) location: string; Column() name: string; } // 查询附近的点 async findNearby(lat: number, lng: number, radius: number) { return this.placesRepository .createQueryBuilder() .where( ST_Distance_Sphere(location, ST_GeomFromText(POINT(${lng} ${lat}))) :radius, { radius }, ) .getMany(); }30. 全文搜索实现使用MySQL全文索引Entity() export class Article { PrimaryGeneratedColumn() id: number; Column() Index({ fulltext: true }) title: string; Column(text) Index({ fulltext: true }) content: string; } // 全文搜索查询 async searchArticles(keyword: string) { return this.articlesRepository .createQueryBuilder() .where( MATCH(title, content) AGAINST(:keyword IN BOOLEAN MODE), { keyword: ${keyword}* }, ) .getMany(); }31. 数据加密方案敏感字段加密策略import { encrypt, decrypt } from ./crypto.util; Entity() export class User { Column({ transformer: { to: (value: string) encrypt(value), from: (value: string) decrypt(value), }, }) privateKey: string; }加密工具示例// src/shared/utils/crypto.util.ts import * as crypto from crypto; const algorithm aes-256-cbc; const key crypto.scryptSync(process.env.ENCRYPTION_KEY, salt, 32); const iv Buffer.alloc(16, 0); export function encrypt(text: string): string { const cipher crypto.createCipheriv(algorithm, key, iv); let encrypted cipher.update(text, utf8, hex); encrypted cipher.final(hex); return encrypted; } export function decrypt(encrypted: string): string { const decipher crypto.createDecipheriv(algorithm, key, iv); let decrypted decipher.update(encrypted, hex, utf8); decrypted decipher.final(utf8); return decrypted; }32. 数据库版本控制使用Flyway管理数据库变更安装Flyway CLI创建迁移脚本目录结构编写SQL迁移文件-- V1__Initial_schema.sql CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL ); -- V2__Add_email_column.sql ALTER TABLE users ADD COLUMN email VARCHAR(255);集成到Nest应用生命周期Injectable() export class FlywayService { async migrate() { const flyway new Flyway({ url: process.env.DATABASE_URL, user: process.env.DATABASE_USER, password: process.env.DATABASE_PASSWORD, locations: [filesystem:./migrations], }); await flyway.migrate(); } } // 在main.ts中调用 async function bootstrap() { const app await NestFactory.create(AppModule); await app.get(FlywayService).migrate(); await app.listen(3000); }33. 读写分离实战配置读写分离连接TypeOrmModule.forRootAsync({ name: write, useFactory: () ({ type: mysql, host: process.env.DB_WRITE_HOST, // ...写库配置 }), }), TypeOrmModule.forRootAsync({ name: read, useFactory: () ({ type: mysql, host: process.env.DB_READ_HOST, // ...读库配置 }), }),使用装饰器选择数据源Injectable() export class UsersService { constructor( InjectRepository(User, write) private writeRepository: RepositoryUser, InjectRepository(User, read) private readRepository: RepositoryUser, ) {} async readOperations() { return this.readRepository.find(); } async writeOperations() { return this.writeRepository.save(new User()); } }34. 连接失败重试策略实现健壮的重连机制async function createConnectionWithRetry(options: ConnectionOptions, maxRetries 3) { let retries 0; while (retries maxRetries) { try { return await createConnection(options); } catch (error) { retries; if (retries maxRetries) throw error; await new Promise(resolve setTimeout(resolve, 1000 * Math.pow(2, retries)) ); console.log(Retrying connection (attempt ${retries})...);
NestJS与MySQL整合开发实战指南
1. Nest框架与MySQL数据库入门指南作为现代Node.js后端开发的主流框架NestJS凭借其模块化设计和TypeScript支持已成为企业级应用开发的热门选择。而MySQL作为最流行的关系型数据库之一与Nest的整合是每个全栈开发者必须掌握的技能。本文将带你从零开始搭建Nest项目并实现完整的MySQL数据库操作流程。提示本文基于Nest 9.x和MySQL 8.0环境所有代码示例均通过TypeScript 4.7验证1.1 环境准备与项目初始化首先确保你的开发环境已安装Node.js 16推荐18.x LTS版本MySQL 8.0社区版即可任意代码编辑器VS Code为首选通过以下命令创建新项目npm i -g nestjs/cli nest new nest-mysql-demo cd nest-mysql-demo安装MySQL驱动和TypeORMNest官方推荐的ORMnpm install nestjs/typeorm typeorm mysql21.2 数据库连接配置在app.module.ts中配置TypeORM连接import { TypeOrmModule } from nestjs/typeorm; Module({ imports: [ TypeOrmModule.forRoot({ type: mysql, host: localhost, port: 3306, username: root, password: your_password, database: nest_demo, entities: [__dirname /**/*.entity{.ts,.js}], synchronize: true, // 开发环境可用生产环境必须关闭 }), ], }) export class AppModule {}警告synchronize: true会基于实体类自动同步数据库结构方便开发但可能造成生产环境数据丢失务必在正式部署时设为false2. 实体定义与CRUD实现2.1 创建第一个实体假设我们要开发用户管理系统先创建用户实体// src/users/user.entity.ts import { Entity, PrimaryGeneratedColumn, Column } from typeorm; Entity() export class User { PrimaryGeneratedColumn() id: number; Column({ length: 50 }) username: string; Column({ select: false }) // 查询时默认不返回密码 password: string; Column({ default: true }) isActive: boolean; Column({ type: timestamp, default: () CURRENT_TIMESTAMP }) createdAt: Date; }2.2 实现服务层和数据访问创建用户模块和服务nest generate module users nest generate service users完善服务逻辑// src/users/users.service.ts import { Injectable } from nestjs/common; import { InjectRepository } from nestjs/typeorm; import { Repository } from typeorm; import { User } from ./user.entity; Injectable() export class UsersService { constructor( InjectRepository(User) private usersRepository: RepositoryUser, ) {} async create(user: PartialUser): PromiseUser { return this.usersRepository.save(user); } async findAll(): PromiseUser[] { return this.usersRepository.find(); } async findOne(id: number): PromiseUser { return this.usersRepository.findOne({ where: { id } }); } async update(id: number, updateData: PartialUser): Promisevoid { await this.usersRepository.update(id, updateData); } async remove(id: number): Promisevoid { await this.usersRepository.delete(id); } }2.3 控制器层实现生成控制器并实现RESTful接口nest generate controller users// src/users/users.controller.ts import { Controller, Get, Post, Body, Param, Put, Delete } from nestjs/common; import { UsersService } from ./users.service; import { User } from ./user.entity; Controller(users) export class UsersController { constructor(private readonly usersService: UsersService) {} Post() create(Body() user: User) { return this.usersService.create(user); } Get() findAll() { return this.usersService.findAll(); } Get(:id) findOne(Param(id) id: string) { return this.usersService.findOne(id); } Put(:id) update(Param(id) id: string, Body() updateData: PartialUser) { return this.usersService.update(id, updateData); } Delete(:id) remove(Param(id) id: string) { return this.usersService.remove(id); } }3. 高级查询与事务处理3.1 复杂查询构建TypeORM提供了强大的查询构建器以下是几个实用示例// 在UsersService中添加方法 async findActiveUsers(): PromiseUser[] { return this.usersRepository .createQueryBuilder(user) .where(user.isActive :isActive, { isActive: true }) .orderBy(user.createdAt, DESC) .getMany(); } async searchUsers(keyword: string): PromiseUser[] { return this.usersRepository .createQueryBuilder() .where(username LIKE :keyword, { keyword: %${keyword}% }) .orWhere(email LIKE :keyword, { keyword: %${keyword}% }) .getMany(); }3.2 事务处理实战对于需要原子性操作的多表写入必须使用事务async transferCredits( fromId: number, toId: number, amount: number, ): Promisevoid { await this.usersRepository.manager.transaction(async (manager) { const fromUser await manager.findOne(User, { where: { id: fromId } }); const toUser await manager.findOne(User, { where: { id: toId } }); if (!fromUser || !toUser) { throw new Error(用户不存在); } if (fromUser.credits amount) { throw new Error(余额不足); } fromUser.credits - amount; toUser.credits amount; await manager.save(User, fromUser); await manager.save(User, toUser); }); }4. 性能优化与生产环境实践4.1 连接池配置优化在高并发场景下合理的连接池配置至关重要TypeOrmModule.forRoot({ // ...其他配置 extra: { connectionLimit: 10, // 连接池大小 waitForConnections: true, queueLimit: 0, }, }),推荐配置规则开发环境5-10个连接生产环境按 (核心数 * 2) 有效磁盘数 计算4.2 索引与查询优化为常用查询字段添加索引Entity() export class User { // ... Column({ length: 50 }) Index(IDX_USERNAME, { unique: true }) username: string; Column() Index(IDX_EMAIL, { unique: true }) email: string; }查询优化技巧避免SELECT *只查询必要字段对分页查询使用skip()和take()替代limit和offset复杂查询考虑使用原生SQL4.3 生产环境部署要点必须关闭synchronize使用迁移管理数据库变更启用SSL连接云数据库必备配置适当的连接超时时间实现健康检查端点监控数据库连接状态// 健康检查示例 Get(health) async healthCheck() { try { await this.usersRepository.query(SELECT 1); return { status: ok }; } catch (error) { throw new HttpException( { status: down, error: error.message }, HttpStatus.SERVICE_UNAVAILABLE, ); } }5. 常见问题排查5.1 连接问题速查表错误现象可能原因解决方案ECONNREFUSED数据库服务未启动检查MySQL服务状态ER_ACCESS_DENIED_ERROR认证失败验证用户名/密码ER_BAD_DB_ERROR数据库不存在创建指定数据库ETIMEDOUT网络问题检查防火墙/安全组规则5.2 性能问题诊断慢查询日志分析-- 在MySQL中启用慢查询日志 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;使用EXPLAIN分析查询计划const query this.usersRepository .createQueryBuilder(user) .where(user.isActive :isActive, { isActive: true }) .getSql(); const explainResult await this.usersRepository.query( EXPLAIN ${query}, );5.3 事务隔离级别问题MySQL默认使用REPEATABLE READ隔离级别可能导致幻读。对于需要更高隔离级别的操作async sensitiveOperation() { await this.usersRepository.manager.transaction( SERIALIZABLE, async (manager) { // 事务操作 }, ); }6. 扩展实践与进阶技巧6.1 多数据库连接配置对于需要连接多个MySQL实例的场景Module({ imports: [ TypeOrmModule.forRoot({ name: primary, type: mysql, // 主数据库配置 }), TypeOrmModule.forRoot({ name: secondary, type: mysql, // 从数据库配置 }), ], }) export class AppModule {} // 使用时指定连接 InjectRepository(User, secondary) private secondaryUserRepository: RepositoryUser6.2 数据迁移管理使用TypeORM迁移工具管理数据库变更安装typeorm扩展npm install ts-node typeorm -D创建迁移npx typeorm migration:create src/migrations/InitSchema编写迁移文件import { MigrationInterface, QueryRunner } from typeorm; export class InitSchema implements MigrationInterface { public async up(queryRunner: QueryRunner): Promisevoid { await queryRunner.query( CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, isActive BOOLEAN DEFAULT TRUE, createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ); } public async down(queryRunner: QueryRunner): Promisevoid { await queryRunner.query(DROP TABLE users); } }运行迁移npx typeorm migration:run6.3 软删除实现通过装饰器轻松实现软删除import { DeleteDateColumn } from typeorm; Entity() export class User { // ... DeleteDateColumn() deletedAt?: Date; } // 查询时自动过滤已删除记录 this.usersRepository.find(); // 需要查询已删除记录时 this.usersRepository.find({ withDeleted: true });7. 安全最佳实践7.1 敏感数据保护密码存储必须加密import * as bcrypt from bcrypt; async create(user: PartialUser): PromiseUser { const salt await bcrypt.genSalt(); user.password await bcrypt.hash(user.password, salt); return this.usersRepository.save(user); }使用select: false隐藏敏感字段实现DTO层过滤返回数据7.2 SQL注入防护TypeORM已内置防护措施但需注意始终使用参数化查询避免直接拼接SQL对用户输入进行严格验证// 安全做法 this.usersRepository .createQueryBuilder() .where(username :username, { username: userInput }); // 危险做法绝对避免 this.usersRepository.query( SELECT * FROM users WHERE username ${userInput}, );7.3 连接安全配置生产环境必须启用SSLTypeOrmModule.forRoot({ ssl: { rejectUnauthorized: true, ca: fs.readFileSync(/path/to/server-ca.pem).toString(), cert: fs.readFileSync(/path/to/client-cert.pem).toString(), key: fs.readFileSync(/path/to/client-key.pem).toString(), }, }),8. 测试策略8.1 单元测试示例使用Jest测试服务层describe(UsersService, () { let service: UsersService; let repository: RepositoryUser; beforeEach(async () { const module: TestingModule await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(User), useClass: Repository, }, ], }).compile(); service module.getUsersService(UsersService); repository module.getRepositoryUser(getRepositoryToken(User)); }); it(should create a user, async () { const userData { username: test, password: 123456 }; jest.spyOn(repository, save).mockResolvedValueOnce({ id: 1, ...userData }); const result await service.create(userData); expect(result).toHaveProperty(id); expect(repository.save).toHaveBeenCalled(); }); });8.2 E2E测试配置测试模块配置使用测试数据库Module({ imports: [ TypeOrmModule.forRoot({ type: mysql, host: localhost, port: 3307, // 测试数据库端口 username: test, password: test, database: test_db, entities: [__dirname /**/*.entity{.ts,.js}], synchronize: true, // 测试环境可以开启 }), UsersModule, ], }) export class TestModule {}9. 性能监控与日志9.1 慢查询监控集成TypeORM日志记录慢查询TypeOrmModule.forRoot({ logging: [query], maxQueryExecutionTime: 1000, // 超过1秒的查询记录为慢查询 }),9.2 自定义查询日志通过订阅TypeORM事件实现高级日志import { Connection, QueryRunner } from typeorm; Injectable() export class DatabaseLogger implements Logger { constructor(private connection: Connection) { this.connection.subscribe((event) { if (event.queryRunner?.data?.queryDuration 500) { console.warn(Slow query detected: ${event.queryRunner.query}); } }); } }10. 项目结构优化建议10.1 模块化组织推荐的项目结构src/ ├── modules/ │ ├── users/ │ │ ├── user.entity.ts │ │ ├── users.service.ts │ │ ├── users.controller.ts │ │ ├── users.module.ts │ │ └── dto/ │ │ ├── create-user.dto.ts │ │ └── update-user.dto.ts ├── shared/ │ ├── database/ │ │ └── database.module.ts │ └── utils/ └── app.module.ts10.2 数据库连接集中管理创建共享的数据库模块// src/shared/database/database.module.ts Module({ imports: [ TypeOrmModule.forRootAsync({ useFactory: () ({ type: mysql, host: process.env.DB_HOST, // ...其他配置 }), }), ], exports: [TypeOrmModule], }) export class DatabaseModule {}11. 实际项目经验分享在大型项目中处理千万级用户表时我们发现几个关键点分库分表策略当单表超过500万行时考虑按用户ID哈希分片读写分离配置多个从库分担读压力缓存层对热点数据使用Redis缓存连接管理使用连接中间件如ProxySQL管理连接池一个实用的连接池监控代码片段setInterval(async () { const pool this.connection.driver.pool; console.log({ total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, }); }, 5000);12. 调试技巧12.1 查询日志输出临时开启完整SQL日志TypeOrmModule.forRoot({ logger: advanced-console, logging: [query, error], }),12.2 事务调试在事务中插入调试点await this.usersRepository.manager.transaction(async (manager) { console.time(transaction); // 业务代码 console.timeEnd(transaction); });13. 版本兼容性说明不同版本组合的注意事项NestJSTypeORMMySQL驱动注意事项9.x0.3.xmysql2 2.3推荐组合8.x0.2.xmysql2 2.2需要手动安装nestjs/typeorm7.x0.2.xmysql 2.18不支持async/await语法14. 替代方案比较14.1 Sequelize vs TypeORM特性TypeORMSequelize语法风格装饰器ActiveRecord传统ORM接口TypeScript支持原生优秀需要额外类型包关联查询更直观稍显复杂社区活跃度高极高迁移工具内置需要单独安装14.2 原生MySQL驱动对于简单场景可以直接使用mysql2包import { createPool } from mysql2/promise; const pool createPool({ host: localhost, user: root, database: test, waitForConnections: true, connectionLimit: 10, }); async function queryUsers() { const [rows] await pool.query(SELECT * FROM users LIMIT 10); return rows; }15. 资源推荐官方文档NestJS TypeORM集成TypeORM MySQL配置性能优化指南MySQL官方优化白皮书《高性能MySQL》第3版监控工具Percona Monitoring and ManagementPrometheus Grafana MySQL仪表板安全审计工具sqlmap用于检测注入漏洞MySQL Enterprise Audit16. 升级与迁移策略从旧版本升级时的注意事项备份优先执行任何升级前完整备份数据库逐步升级先升级开发环境验证后再升级生产变更日志仔细阅读TypeORM和Nest的CHANGELOG测试覆盖确保有足够的测试用例验证关键功能典型升级步骤# 1. 备份package.json # 2. 更新依赖 npm install nestjs/typeormlatest typeormlatest mysql2latest # 3. 运行测试 npm test # 4. 检查弃用警告17. 云数据库实践连接云数据库如AWS RDS的特殊配置TypeOrmModule.forRoot({ type: mysql, host: process.env.RDS_HOSTNAME, port: parseInt(process.env.RDS_PORT), username: process.env.RDS_USERNAME, password: process.env.RDS_PASSWORD, database: process.env.RDS_DB_NAME, ssl: { rejectUnauthorized: true, ca: fs.readFileSync(/path/to/rds-combined-ca-bundle.pem).toString(), }, }),云环境最佳实践使用IAM数据库认证替代密码配置自动扩展的连接池启用数据库代理减少连接开销设置适当的自动备份策略18. 连接问题深度排查当遇到间歇性连接断开时可按以下步骤排查检查MySQL服务器端的wait_timeout设置验证TCP keepalive配置检查网络设备负载均衡器、防火墙的超时设置在TypeORM中配置连接验证TypeOrmModule.forRoot({ // ... extra: { connectionTimeout: 30000, idleTimeout: 10000, keepAliveInitialDelay: 10000, }, }),19. 批量操作优化处理大量数据时的性能技巧使用批量插入代替循环插入async bulkCreate(users: User[]) { await this.usersRepository .createQueryBuilder() .insert() .into(User) .values(users) .execute(); }分批次处理大数据集const batchSize 1000; for (let i 0; i total; i batchSize) { const batch data.slice(i, i batchSize); await this.bulkCreate(batch); }禁用日志提升批量操作性能await this.usersRepository.manager.transaction(async (manager) { await manager.query(SET FOREIGN_KEY_CHECKS 0); await manager.query(SET UNIQUE_CHECKS 0); await manager.query(SET AUTOCOMMIT 0); // 批量操作 await manager.query(SET FOREIGN_KEY_CHECKS 1); await manager.query(SET UNIQUE_CHECKS 1); await manager.query(SET AUTOCOMMIT 1); });20. 数据类型最佳实践MySQL与TypeScript类型映射建议MySQL类型TypeORM装饰器TypeScript类型注意事项INTColumn(int)number主键推荐使用PrimaryGeneratedColumn()VARCHARColumn(varchar, { length: 255 })string根据实际需求设置长度TEXTColumn(text)string大文本内容使用BOOLEANColumn(boolean)booleanMySQL实际存储为TINYINT(1)DATETIMEColumn(datetime)Date推荐使用TIMESTAMP自动管理时间DECIMALColumn(decimal, { precision: 10, scale: 2 })string避免使用float处理精确计算JSONColumn(json)object自动序列化/反序列化21. 关联关系实战实现用户与文章的1对多关系// user.entity.ts OneToMany(() Article, (article) article.author) articles: Article[]; // article.entity.ts ManyToOne(() User, (user) user.articles) JoinColumn({ name: author_id }) author: User;查询关联数据的几种方式使用relations选项this.usersRepository.find({ relations: [articles] });使用查询构建器this.usersRepository .createQueryBuilder(user) .leftJoinAndSelect(user.articles, articles) .where(user.id :id, { id: 1 }) .getOne();使用RelationId获取外键RelationId((article: Article) article.author) authorId: number;22. 自定义仓储模式扩展默认Repository功能// src/users/users.repository.ts EntityRepository(User) export class UsersRepository extends RepositoryUser { async findActiveUsers(): PromiseUser[] { return this.find({ where: { isActive: true } }); } } // 在模块中注册 Module({ imports: [TypeOrmModule.forFeature([User, UsersRepository])], providers: [UsersService], controllers: [UsersController], }) export class UsersModule {} // 在服务中使用 Injectable() export class UsersService { constructor( InjectRepository(User) private usersRepository: UsersRepository, ) {} }23. 数据库健康检查实现全面的健康检查端点Get(health) async healthCheck() { const dbCheck await this.checkDatabase(); const redisCheck await this.checkRedis(); return { status: dbCheck redisCheck ? healthy : degraded, details: { database: dbCheck ? up : down, redis: redisCheck ? up : down, timestamp: new Date().toISOString(), }, }; } private async checkDatabase(): Promiseboolean { try { await this.usersRepository.query(SELECT 1); return true; } catch { return false; } }24. 连接池监控与管理高级连接池配置示例TypeOrmModule.forRoot({ // ...其他配置 extra: { poolSize: 20, acquireTimeout: 30000, idleTimeout: 10000, queueLimit: 0, enableKeepAlive: true, keepAliveInitialDelay: 10000, }, }),监控连接池状态的实用方法Injectable() export class DatabaseMonitorService { constructor(private connection: Connection) {} getPoolStats() { const pool this.connection.driver.pool; return { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, max: pool.options.connectionLimit, }; } }25. 生产环境部署清单上线前必须检查的项目安全配置禁用synchronize启用SSL连接使用强密码和最小权限账户性能配置优化连接池大小设置适当的查询超时配置慢查询日志监控配置实现健康检查端点设置数据库性能监控配置告警规则备份策略定期全量备份二进制日志增量备份测试恢复流程灾难恢复多可用区部署故障转移演练回滚方案准备26. 性能基准测试使用Jest进行简单的性能测试describe(Performance Tests, () { it(should handle 100 concurrent queries, async () { const start Date.now(); const promises Array(100) .fill(0) .map(() usersService.findAll()); await Promise.all(promises); const duration Date.now() - start; console.log(100 queries took ${duration}ms); expect(duration).toBeLessThan(1000); }); });关键性能指标参考值操作类型单次操作预期耗时并发能力简单查询10ms1000 QPS复杂联查50-100ms100 QPS插入操作20-50ms500 QPS更新操作30-60ms300 QPS27. 连接泄漏排查检测和修复连接泄漏的步骤监控连接数增长setInterval(() { const pool connection.driver.pool; console.log(Connections: ${pool.totalCount}/${pool.options.connectionLimit}); }, 1000);使用AsyncLocalStorage跟踪连接获取import { AsyncLocalStorage } from async_hooks; const connectionStorage new AsyncLocalStorage(); async function withConnectionT(fn: () PromiseT): PromiseT { return connectionStorage.run(new Date(), async () { try { return await fn(); } finally { const start connectionStorage.getStore(); if (start Date.now() - start 1000) { console.warn(Long running connection detected); } } }); }常见泄漏场景未处理异常的异步操作忘记释放的手动事务循环中未关闭的游标28. 多租户实现使用TypeORM实现多租户架构// 租户感知的Repository EntityRepository(User) export class TenantAwareRepository extends RepositoryUser { async findForTenant(tenantId: string): PromiseUser[] { return this.createQueryBuilder(user) .where(user.tenantId :tenantId, { tenantId }) .getMany(); } } // 请求作用域的租户ID Injectable({ scope: Scope.REQUEST }) export class TenantService { tenantId: string; } // 在服务中使用 Injectable() export class UsersService { constructor( InjectRepository(User) private usersRepository: TenantAwareRepository, private tenantService: TenantService, ) {} async findAll(): PromiseUser[] { return this.usersRepository.findForTenant(this.tenantService.tenantId); } }29. 地理空间数据处理MySQL支持的地理空间功能示例Entity() export class Place { PrimaryGeneratedColumn() id: number; Column(point) location: string; Column() name: string; } // 查询附近的点 async findNearby(lat: number, lng: number, radius: number) { return this.placesRepository .createQueryBuilder() .where( ST_Distance_Sphere(location, ST_GeomFromText(POINT(${lng} ${lat}))) :radius, { radius }, ) .getMany(); }30. 全文搜索实现使用MySQL全文索引Entity() export class Article { PrimaryGeneratedColumn() id: number; Column() Index({ fulltext: true }) title: string; Column(text) Index({ fulltext: true }) content: string; } // 全文搜索查询 async searchArticles(keyword: string) { return this.articlesRepository .createQueryBuilder() .where( MATCH(title, content) AGAINST(:keyword IN BOOLEAN MODE), { keyword: ${keyword}* }, ) .getMany(); }31. 数据加密方案敏感字段加密策略import { encrypt, decrypt } from ./crypto.util; Entity() export class User { Column({ transformer: { to: (value: string) encrypt(value), from: (value: string) decrypt(value), }, }) privateKey: string; }加密工具示例// src/shared/utils/crypto.util.ts import * as crypto from crypto; const algorithm aes-256-cbc; const key crypto.scryptSync(process.env.ENCRYPTION_KEY, salt, 32); const iv Buffer.alloc(16, 0); export function encrypt(text: string): string { const cipher crypto.createCipheriv(algorithm, key, iv); let encrypted cipher.update(text, utf8, hex); encrypted cipher.final(hex); return encrypted; } export function decrypt(encrypted: string): string { const decipher crypto.createDecipheriv(algorithm, key, iv); let decrypted decipher.update(encrypted, hex, utf8); decrypted decipher.final(utf8); return decrypted; }32. 数据库版本控制使用Flyway管理数据库变更安装Flyway CLI创建迁移脚本目录结构编写SQL迁移文件-- V1__Initial_schema.sql CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL ); -- V2__Add_email_column.sql ALTER TABLE users ADD COLUMN email VARCHAR(255);集成到Nest应用生命周期Injectable() export class FlywayService { async migrate() { const flyway new Flyway({ url: process.env.DATABASE_URL, user: process.env.DATABASE_USER, password: process.env.DATABASE_PASSWORD, locations: [filesystem:./migrations], }); await flyway.migrate(); } } // 在main.ts中调用 async function bootstrap() { const app await NestFactory.create(AppModule); await app.get(FlywayService).migrate(); await app.listen(3000); }33. 读写分离实战配置读写分离连接TypeOrmModule.forRootAsync({ name: write, useFactory: () ({ type: mysql, host: process.env.DB_WRITE_HOST, // ...写库配置 }), }), TypeOrmModule.forRootAsync({ name: read, useFactory: () ({ type: mysql, host: process.env.DB_READ_HOST, // ...读库配置 }), }),使用装饰器选择数据源Injectable() export class UsersService { constructor( InjectRepository(User, write) private writeRepository: RepositoryUser, InjectRepository(User, read) private readRepository: RepositoryUser, ) {} async readOperations() { return this.readRepository.find(); } async writeOperations() { return this.writeRepository.save(new User()); } }34. 连接失败重试策略实现健壮的重连机制async function createConnectionWithRetry(options: ConnectionOptions, maxRetries 3) { let retries 0; while (retries maxRetries) { try { return await createConnection(options); } catch (error) { retries; if (retries maxRetries) throw error; await new Promise(resolve setTimeout(resolve, 1000 * Math.pow(2, retries)) ); console.log(Retrying connection (attempt ${retries})...);