@olimjonkamalov/nest-starter
v0.1.1
Published
NestJS backend starter kit: pagination, {data,meta} javob formati, 422 validatsiya xatolari, rol/auth dekoratorlari va numeric(14,2) uchun aniq pul arifmetikasi
Downloads
119
Maintainers
Readme
@olimjonkamalov/nest-starter
NestJS backend uchun tayyor global qatlam: sahifalash, yagona javob va xato
formati, auth dekoratorlari, numeric(14,2) uchun aniq pul arifmetikasi va
deploy shablonlari (Docker / nginx / PM2 / GitLab CI).
Har bir yangi loyihada qayta yoziladigan kodni bir marta o'rnatib ishlatish uchun.
npm i @olimjonkamalov/nest-starterMundarija
- Tez boshlash
- Sahifalash (pagination)
- Javob formati
- Xato formati
- Validatsiya
- Auth: rollar va dekoratorlar
- Pul va
numeric(14,2) - TypeORM transformerlari
- Yordamchi funksiyalar
- Deploy shablonlari: Docker, nginx, PM2, CI
- To'liq API ro'yxati
- npm ga chiqarish
Tez boshlash
main.ts da bitta chaqiruv butun HTTP qatlamini standartga soladi:
import { NestFactory } from '@nestjs/core';
import { setupApp } from '@olimjonkamalov/nest-starter';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
setupApp(app, {
globalPrefix: 'api/v1',
corsOrigins: process.env.CORS_ORIGINS?.split(','),
});
await app.listen(process.env.PORT ?? 8000);
}
bootstrap();setupApp quyidagilarni ulaydi:
| Nima | Natija |
|---|---|
| createValidationPipe() | Validatsiya xatolari 422 va { maydon: xabar } ko'rinishida |
| AllExceptionsFilter | Barcha xatolar yagona formatda, 500 da ichki tafsilot sizib chiqmaydi |
| ResponseInterceptor | Muvaffaqiyatli javoblar { data: ... } ichida |
| setGlobalPrefix | api/v1 |
| enableCors | Faqat ro'yxatdagi domenlar |
Har birini alohida ham ulash mumkin (pastda).
Sahifalash (pagination)
1. DTO
import { PaginationQueryDto } from '@olimjonkamalov/nest-starter';
export class QueryProductsDto extends PaginationQueryDto {
// page, per_page, sort, include allaqachon bor
}Qabul qilinadi: ?page=2&per_page=50&sort=-created_at.
per_page maksimum 200 (bitta so'rov bilan butun bazani tortib
ololmaydi), default 20.
2. Servisda
import { paginate, PaginatedResult } from '@olimjonkamalov/nest-starter';
const SORTABLE = {
id: 'product.id',
name: 'product.name',
price: 'product.price',
created_at: 'product.createdAt',
};
async findAll(query: QueryProductsDto): Promise<PaginatedResult<ProductDto>> {
const qb = this.repo.createQueryBuilder('product')
.leftJoinAndSelect('product.category', 'category');
if (query.filter?.search) {
qb.andWhere('product.name ILIKE :s', { s: `%${query.filter.search}%` });
}
return paginate(qb, {
...query,
sortable: SORTABLE,
defaultSort: { column: 'product.createdAt', direction: 'DESC' },
tieBreaker: { column: 'product.id' },
map: toProductResponse,
});
}sortable — xavfsizlik chegarasi. Ro'yxatda yo'q ustun nomi butunlay
e'tiborsiz qoldiriladi va defaultSort ishlatiladi, ya'ni
?sort=id;DROP TABLE kabi qiymat SQL'ga tushmaydi.
tieBreaker nima uchun kerak. Faqat created_at bo'yicha saralaganda bir
xil soniyada yaratilgan yozuvlar tartibi har so'rovda o'zgarishi mumkin —
natijada 2-sahifada 1-sahifadagi yozuv qayta chiqadi yoki umuman tushib
qoladi. tieBreaker: { column: 'product.id' } buni yopadi.
3. Javob
{
"data": [ { "id": 1, "name": "..." } ],
"meta": {
"current_page": 2,
"per_page": 20,
"total": 53,
"last_page": 3,
"from": 21,
"to": 40
}
}Qo'lda qurish
QueryBuilder ishlatmasangiz (findAndCount, xom SQL):
import { buildPaginatedResult, getSkipTake } from '@olimjonkamalov/nest-starter';
const { skip, take, page, perPage } = getSkipTake(query.page, query.per_page);
const [items, total] = await this.repo.findAndCount({ skip, take });
return buildPaginatedResult(items.map(toDto), total, page, perPage);Javob formati
ResponseInterceptor yagona shaklni kafolatlaydi — frontend har doim
response.data.data dan o'qiydi:
return { id: 1 }; // -> { "data": { "id": 1 } }
return [1, 2]; // -> { "data": [1, 2] }
return paginate(...); // -> { "data": [...], "meta": {...} } (o'ralmaydi)Javobda allaqachon data kaliti bo'lsa, interceptor unga TEGMAYDI — shuning
uchun PaginatedResult ikki marta o'ralib qolmaydi.
Xato formati
// 422 — maydon xatolari
{
"data": { "phone": "Bu raqam band" },
"message": "Ma'lumotlar noto'g'ri",
"msg": "Ma'lumotlar noto'g'ri",
"errorMessage": "Ma'lumotlar noto'g'ri"
}
// qolgan holatlar
{
"message": "Topilmadi",
"msg": "Topilmadi",
"errorMessage": "Topilmadi",
"statusCode": 404
}message uch kalitda takrorlanadi (message / msg / errorMessage) —
turli frontend kutubxonalari turlichasini o'qiydi, natijada hech qayerda
"undefined" chiqmaydi.
500 xatolarida ichki xabar tashqariga chiqmaydi. SQL matni, stack va boshqa tafsilotlar faqat logga yoziladi, foydalanuvchi esa umumiy xabar ko'radi.
Maydon xatosini qo'lda otish
import { ValidationException } from '@olimjonkamalov/nest-starter';
if (await this.repo.exists({ where: { phone } })) {
throw new ValidationException({ phone: 'Bu raqam allaqachon band' });
}Mashina o'qiydigan kod bilan
import { CodedException } from '@olimjonkamalov/nest-starter';
throw new CodedException('SUBSCRIPTION_EXPIRED', 'Obuna muddati tugagan', 403);
// -> { message, msg, errorMessage, statusCode: 403, code: "SUBSCRIPTION_EXPIRED" }Frontend interceptor'i code bo'yicha maxsus muomala qiladi (masalan
foydalanuvchini obuna sahifasiga yo'naltiradi), matnni tahlil qilmaydi.
Xabarlarni o'zgartirish
app.useGlobalFilters(
new AllExceptionsFilter({
serverErrorMessage: 'Server error',
validationMessage: 'Validation failed',
databaseErrorMessage: 'Query failed',
}),
);Validatsiya
import { createValidationPipe } from '@olimjonkamalov/nest-starter';
app.useGlobalPipes(createValidationPipe());whitelist: true— DTO'da e'lon qilinmagan maydonlar tashlab yuboriladi- Status 422 (NestJS default 400 emas)
- Ichma-ich xatolar nuqtali yo'lga yoyiladi:
{ "filter.search": "matn kiriting" } - Bitta maydonda bir nechta qoida buzilsa — birinchi xabar (foydalanuvchiga ro'yxat emas, bitta aniq gap kerak)
Auth: rollar va dekoratorlar
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from '@olimjonkamalov/nest-starter';
@Module({
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard }, // o'zingizniki
{ provide: APP_GUARD, useClass: RolesGuard },
],
})
export class AppModule {}import { CurrentUser, Public, Roles, AuthenticatedUser } from '@olimjonkamalov/nest-starter';
@Roles('superadmin', 'admin')
@Controller('users')
export class UsersController {
@Public() // auth talab qilinmaydi
@Post('login')
login() {}
@Roles('superadmin') // klass darajasidagini ALMASHTIRADI
@Delete(':id')
remove() {}
@Get('me')
me(@CurrentUser() user: AuthenticatedUser) {
return user;
}
@Get('my-id')
myId(@CurrentUser('id') id: number) {
return id;
}
}RolesGuard getAllAndOverride ishlatadi: metoddagi @Roles klassdagini
almashtiradi, qo'shmaydi. Ya'ni umumiy ruxsatli kontrollerda bitta
metodni torroq qilib qo'yish mumkin.
@Public() ishlashi uchun auth guard'ingiz IS_PUBLIC_KEY ni tekshirishi
kerak:
import { IS_PUBLIC_KEY } from '@olimjonkamalov/nest-starter';
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}Pul va numeric(14,2)
Pulni hech qachon number da hisoblamang: 0.1 + 0.2 === 0.30000000000000004.
Xato tiyin darajasida boshlanadi va hisobotlarda yig'ilib boradi.
Bu paketdagi amallar BigInt ustida ishlaydi va natija har doim ikki xonali
kasrli string bo'ladi.
import {
addDecimals, subtractDecimals, multiplyDecimals,
compareDecimals, sumDecimals, toDecimal,
} from '@olimjonkamalov/nest-starter';
addDecimals('0.10', '0.20'); // "0.30"
subtractDecimals('100.00', '150.50'); // "-50.50"
multiplyDecimals('12.50', '3'); // "37.50"
sumDecimals(['10.10', '20.20']); // "30.30"
compareDecimals('10.00', '9.99'); // 1
toDecimal(5); // "5.00"Kirish qiymati 2 xonadan ortiq bo'lsa qisqaradi — bu numeric(14,2)
ustunining o'z semantikasi (toDecimal('33.335') === '33.33').
Entity
import { decimalTransformer } from '@olimjonkamalov/nest-starter';
@Column({ type: 'numeric', precision: 14, scale: 2, transformer: decimalTransformer })
amount: string; // number EMASDTO
import { IsDecimalAmount, toDecimalString } from '@olimjonkamalov/nest-starter';
export class CreatePaymentDto {
@Transform(({ value }) => toDecimalString(value))
@IsDecimalAmount({ min: 0, exclusiveMin: true })
amount: string;
}TypeORM transformerlari
import { dateTransformer } from '@olimjonkamalov/nest-starter';
@Column({ type: 'date', transformer: dateTransformer, nullable: true })
due_date: string | null; // "2026-09-24"pg drayveri date ustunini JS Date ga aylantiradi va vaqt zonasi tufayli
kun bir kunga siljib ketishi mumkin. Bu transformer qiymatni har doim
"YYYY-MM-DD" string sifatida saqlaydi.
Yordamchi funksiyalar
import {
toBoolean, parseDurationToSeconds,
formatMoney, formatDate, escapeHtml,
normalizePhone, isValidPhone, formatPhone,
} from '@olimjonkamalov/nest-starter';
toBoolean('1'); // true (query string uchun)
parseDurationToSeconds('7d'); // 604800
formatMoney('45000.00'); // "45 000"
formatDate('2026-09-24'); // "24.09.2026"
escapeHtml('<b>a</b>'); // "<b>a</b>"
normalizePhone('90 123 45 67'); // "998901234567"
formatPhone('998901234567'); // "+998 90 123-45-67"escapeHtml — Telegram parse_mode: HTML ga foydalanuvchi matnini
qo'yishdan oldin shart, aks holda ismdagi < xabarni buzadi.
toBoolean bilan query filtri:
@Transform(({ value }) => toBoolean(value))
@IsOptional()
@IsBoolean()
is_active?: boolean; // ?filter[is_active]=1Deploy shablonlari
Shablonlarni loyihangizga ko'chirish:
npx @olimjonkamalov/nest-starter init all # hammasi
npx @olimjonkamalov/nest-starter init docker # faqat Docker
npx @olimjonkamalov/nest-starter init nginx
npx @olimjonkamalov/nest-starter init pm2
npx @olimjonkamalov/nest-starter init ci
npx @olimjonkamalov/nest-starter init env
npx @olimjonkamalov/nest-starter init all --force # mavjud fayllarni ham qayta yozishMavjud fayl qayta yozilmaydi (--force bo'lmasa) — loyihangizdagi
sozlamalar tasodifan yo'qolmaydi.
Nima ko'chadi:
| Guruh | Fayl | Qayerga |
|---|---|---|
| docker | Dockerfile, docker-compose.yml, .dockerignore | ildiz |
| nginx | api.conf, spa.conf | deploy/nginx/ |
| pm2 | ecosystem.config.js | ildiz |
| ci | .gitlab-ci.yml | ildiz |
| env | .env.example | ildiz |
Docker
Ikki bosqichli (multi-stage) build: birinchi bosqichda kod kompilyatsiya
qilinadi, ikkinchisiga faqat dist va production paketlari ko'chadi —
image hajmi bir necha barobar kichik bo'ladi.
- root ostida ishlamaydi (alohida
appfoydalanuvchisi) HEALTHCHECK— orkestrator konteyner tirikligini biladiuploads/volume'da — deploy'da yuklangan fayllar yo'qolmaydidocker-compose.ymlda Postgres porti faqat127.0.0.1ga ochilgan, ya'ni internetdan bazaga to'g'ridan-to'g'ri kirib bo'lmaydi
cp .env.example .env # to'ldiring
docker compose up -d
docker compose logs -f apinginx
deploy/nginx/api.conf — API uchun reverse proxy, spa.conf — frontend build
uchun. <API_DOMAIN>, <APP_DOMAIN>, <APP_DIR>, <DIST_DIR> larni
almashtiring.
sudo cp deploy/nginx/api.conf /etc/nginx/sites-available/api.conf
sudo ln -s /etc/nginx/sites-available/api.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.example.comSPA konfigida index.html keshlanmaydi, /assets/ esa bir yil
keshlanadi — aks holda foydalanuvchi deploy'dan keyin ham eski bundle'da
qolib ketadi.
PM2
pm2 start ecosystem.config.js --env production
pm2 save && pm2 startup # server qayta yuklansa avtomatik ko'tariladi
pm2 logs api --lines 100
pm2 restart api --update-env # .env o'zgarganda SHART--update-env siz PM2 eski muhit o'zgaruvchilarini xotirada saqlab qoladi va
.env dagi yangi qiymat ishlamaydi — eng ko'p vaqt yeydigan xatolardan biri.
GitLab CI
.gitlab-ci.yml — main ga merge bo'lganda serverga rsync + npm ci +
build + migratsiya + pm2 restart. Variables: SSH_HOST, SSH_USER,
SSH_PASS, APP_DIR.
To'liq API ro'yxati
Sahifalash: PaginationQueryDto, paginate, buildPaginatedResult,
emptyPaginatedResult, parseSort, getSkipTake, PaginatedResult,
PaginationMeta, SortableColumns, ParsedSort, SortDirection
HTTP: setupApp, ResponseInterceptor, AllExceptionsFilter,
createValidationPipe, flattenValidationErrors, ValidationException,
CodedException, FieldErrors
Auth: Public, IS_PUBLIC_KEY, Roles, ROLES_KEY, RolesGuard,
CurrentUser, AuthenticatedUser
Pul: toDecimal, addDecimals, subtractDecimals, multiplyDecimals,
negateDecimal, sumDecimals, compareDecimals, isDecimalNegative,
isDecimalZero, decimalTransformer, toDecimalString, IsDecimalAmount
TypeORM: dateTransformer
Utils: toBoolean, parseDurationToSeconds, formatMoney, formatDate,
escapeHtml, normalizePhone, isValidPhone, formatPhone
Talablar
Node.js 18+. Peer dependency sifatida (loyihangizda allaqachon bor):
@nestjs/common, @nestjs/core, class-validator, class-transformer,
rxjs, reflect-metadata, typeorm (ixtiyoriy — faqat transformerlar
uchun).
npm ga chiqarish
# 1) Bir marta: npm akkauntiga kirish
npm login
# 2) Nomni tekshirish (404 = bo'sh)
npm view @olimjonkamalov/nest-starter
# 3) Nashrdan oldin — testlar va build (prepublishOnly buni o'zi qiladi)
npm test
npm run build
# 4) Paket ichiga nima tushishini KO'RISH (nashr qilmasdan)
npm pack --dry-run
# 5) Nashr
npm publish --access public
# 6) Keyingi versiyalar (semver)
npm version patch # 0.1.0 -> 0.1.1 (xato tuzatish)
npm version minor # 0.1.0 -> 0.2.0 (yangi imkoniyat)
npm version major # 0.1.0 -> 1.0.0 (buzuvchi o'zgarish)
git push --follow-tags
npm publishPaket scoped (@olimjonkamalov/...) — shuning uchun --access public
SHART, aks holda npm uni private qilib nashr qilmoqchi bo'ladi va bepul
akkauntda xato beradi. publishConfig.access: public allaqachon qo'yilgan.
Nashrdan oldin boshqa loyihada sinab ko'rish:
npm pack # olimjonkamalov-nest-starter-0.1.1.tgz
cd ../boshqa-loyiha
npm i ../nest-starter/olimjonkamalov-nest-starter-0.1.1.tgzLitsenziya
MIT
