API Response formatini standartlashtirish

~15 daq · Express · API Arxitekturasi

Muammodan boshlaylik

Bir loyihada turli endpointlar turli formatda javob qaytaradi:

// Birinchi dasturchi — foydalanuvchilar
res.json({ users: [...], total: 50 });

// Ikkinchi dasturchi — mahsulotlar
res.json([...]);  // ← to'g'ridan-to'g'RI massiv

// Uchinchi dasturchi — buyurtmalar
res.json({ data: {...}, status: 'ok' });

// Xato holatlari ham har xil:
res.status(404).json({ message: 'Topilmadi' });
res.status(400).json({ error: 'Noto\'g\'ri' });
res.status(422).json({ errors: [...] });

Frontend dasturchi har endpoint uchun alohida kod yozishi kerak:

// ❌ — har yerda turli tekshiruv
const res1 = await fetch('/api/users');
const data1 = await res1.json();
const users = data1.users; // ← bu yerda .users

const res2 = await fetch('/api/products');
const data2 = await res2.json();
const products = data2; // ← bu yerda to'g'ridan-to'g'RI massiv?

const res3 = await fetch('/api/orders');
const data3 = await res3.json();
const orders = data3.data; // ← bu yerda .data

Standart format bo'lsa — bir marta yozilgan kod hamma yerda ishlaydi.


Standart format

Eng keng tarqalgan yondashuv:

{
  "success": true,
  "data": { ... },
  "message": "Muvaffaqiyatli",
  "meta": {
    "page": 1,
    "total": 100
  }
}

Xato uchun:

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Foydalanuvchi topilmadi"
  }
}

Javob yordamchi funksiyalari

Har endpointda bir xil strukturani qo'lda yozish o'rniga — yordamchi funksiyalar:

// utils/response.js

const successResponse = (res, data, message = 'Muvaffaqiyatli', statusCode = 200) => {
  return res.status(statusCode).json({
    success: true,
    message,
    data,
  });
};

const createdResponse = (res, data, message = 'Yaratildi') => {
  return successResponse(res, data, message, 201);
};

const errorResponse = (res, message, statusCode = 400, code = null) => {
  return res.status(statusCode).json({
    success: false,
    error: {
      code: code || statusCodeToErrorCode(statusCode),
      message,
    },
  });
};

const notFoundResponse = (res, message = 'Topilmadi') => {
  return errorResponse(res, message, 404, 'NOT_FOUND');
};

const unauthorizedResponse = (res, message = 'Autentifikatsiya kerak') => {
  return errorResponse(res, message, 401, 'UNAUTHORIZED');
};

const forbiddenResponse = (res, message = 'Ruxsat yo\'q') => {
  return errorResponse(res, message, 403, 'FORBIDDEN');
};

const validationErrorResponse = (res, errors) => {
  return res.status(422).json({
    success: false,
    error: {
      code: 'VALIDATION_ERROR',
      message: 'Kiritilgan ma\'lumotlarda xato',
      details: errors,              // ← qaysi maydon, qanday xato
    },
  });
};

// Ichki yordamchi
const statusCodeToErrorCode = (code) => {
  const map = { 400: 'BAD_REQUEST', 401: 'UNAUTHORIZED', 403: 'FORBIDDEN',
                404: 'NOT_FOUND', 409: 'CONFLICT', 500: 'SERVER_ERROR' };
  return map[code] || 'ERROR';
};

module.exports = {
  successResponse,
  createdResponse,
  errorResponse,
  notFoundResponse,
  unauthorizedResponse,
  forbiddenResponse,
  validationErrorResponse,
};

Endpointlarda ishlatish

const {
  successResponse,
  createdResponse,
  notFoundResponse,
  validationErrorResponse,
} = require('../utils/response');

// GET — barcha mahsulotlar
router.get('/', async (req, res, next) => {
  try {
    const products = await Product.find();
    return successResponse(res, products);
    // → { success: true, message: "Muvaffaqiyatli", data: [...] }
  } catch (err) {
    next(err);
  }
});

// GET — bitta mahsulot
router.get('/:id', async (req, res, next) => {
  try {
    const product = await Product.findById(req.params.id);

    if (!product) {
      return notFoundResponse(res, 'Mahsulot topilmadi');
      // → { success: false, error: { code: "NOT_FOUND", message: "..." } }
    }

    return successResponse(res, product);
  } catch (err) {
    next(err);
  }
});

// POST — yangi mahsulot
router.post('/', async (req, res, next) => {
  try {
    const { name, price } = req.body;

    // Validatsiya
    const errors = [];
    if (!name) errors.push({ field: 'name', message: 'Nom majburiy' });
    if (!price) errors.push({ field: 'price', message: 'Narx majburiy' });
    if (price < 0) errors.push({ field: 'price', message: 'Narx manfiy bo\'lmasligi kerak' });

    if (errors.length > 0) {
      return validationErrorResponse(res, errors);
      // → { success: false, error: { code: "VALIDATION_ERROR", details: [...] } }
    }

    const product = await Product.create({ name, price });
    return createdResponse(res, product, 'Mahsulot yaratildi');
    // → { success: true, message: "Mahsulot yaratildi", data: {...} }
  } catch (err) {
    next(err);
  }
});

Pagination uchun meta

Ro'yxat endpointlarida sahifalash ma'lumoti ham kerak:

// utils/response.js ga qo'shimcha
const paginatedResponse = (res, data, meta) => {
  return res.status(200).json({
    success: true,
    data,
    meta: {
      page: meta.page,
      limit: meta.limit,
      total: meta.total,
      totalPages: Math.ceil(meta.total / meta.limit),
      hasNextPage: meta.page < Math.ceil(meta.total / meta.limit),
      hasPrevPage: meta.page > 1,
    },
  });
};
router.get('/', async (req, res, next) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const skip = (page - 1) * limit;

    const [products, total] = await Promise.all([
      Product.find().skip(skip).limit(limit),
      Product.countDocuments(),
    ]);

    return paginatedResponse(res, products, { page, limit, total });
    /*
    {
      "success": true,
      "data": [...],
      "meta": {
        "page": 1,
        "limit": 10,
        "total": 85,
        "totalPages": 9,
        "hasNextPage": true,
        "hasPrevPage": false
      }
    }
    */
  } catch (err) {
    next(err);
  }
});

Global xato handler bilan integratsiya

// middleware/errorHandler.js
const { errorResponse } = require('../utils/response');

const errorHandler = (err, req, res, next) => {
  console.error(err);

  // Mongoose validatsiya xatosi
  if (err.name === 'ValidationError') {
    const errors = Object.values(err.errors).map(e => ({
      field: e.path,
      message: e.message,
    }));
    return res.status(422).json({
      success: false,
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Validatsiya xatosi',
        details: errors,
      },
    });
  }

  // Mongoose CastError — noto'g'ri ID format
  if (err.name === 'CastError') {
    return errorResponse(res, 'Noto\'g\'ri ID format', 400, 'INVALID_ID');
  }

  // MongoDB duplicate key
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return errorResponse(res, `${field} allaqachon mavjud`, 409, 'DUPLICATE');
  }

  // JWT xatolari
  if (err.name === 'JsonWebTokenError') {
    return errorResponse(res, 'Noto\'g\'ri token', 401, 'INVALID_TOKEN');
  }

  if (err.name === 'TokenExpiredError') {
    return errorResponse(res, 'Token muddati o\'tgan', 401, 'TOKEN_EXPIRED');
  }

  // Umumiy server xatosi
  return errorResponse(res, 'Serverda xato yuz berdi', 500, 'SERVER_ERROR');
};

module.exports = errorHandler;
// app.js
const errorHandler = require('./middleware/errorHandler');

app.use('/api/v1', require('./routes/v1'));

app.use(errorHandler); // ← eng oxirida

Frontend da bir marta yozilgan kod

Standart format bo'lsa frontend ham soddalashadi:

// frontend/api.js — universal so'rov funksiyasi
const apiRequest = async (url, options = {}) => {
  const res = await fetch(url, {
    headers: { 'Content-Type': 'application/json' },
    ...options,
  });

  const json = await res.json();

  if (!json.success) {
    // Xato kodi bo'yicha qaror
    if (json.error.code === 'UNAUTHORIZED') {
      window.location.href = '/login';
      return;
    }
    throw new Error(json.error.message);
  }

  return json.data;  // ← har doim .data
};

// Ishlatish — hamma endpoint bir xil
const users = await apiRequest('/api/v1/users');
const product = await apiRequest('/api/v1/products/5');
const newOrder = await apiRequest('/api/v1/orders', {
  method: 'POST',
  body: JSON.stringify({ productId: 5, qty: 2 }),
});

Tez-tez uchraydigan xatolar

1. success: true bilan xato status kodi

// ❌
res.status(404).json({ success: true, data: null });

Sababi: success: true va 404 ziddiyatli. Frontend status kodga ham, success ga ham qaraydi — ikkalasi mos kelishi kerak.

2. Har xil maydon nomlaridan foydalanish

// ❌ — ba'zi endpointlarda result, ba'zilarida data
res.json({ result: users });
res.json({ data: products });
res.json({ items: orders });

Sababi: frontend data kutadi, lekin result keladi — undefined. Butun loyiha bo'yicha bir xil nom ishlatilsin.

3. Xato ma'lumotini foydalanuvchiga ko'rsatish

// ❌ — server ichki xatosi foydalanuvchiga ko'rinadi
res.status(500).json({ error: err.message });
// → "Cannot read properties of undefined (reading 'id')"
// ✅ — foydalanuvchiga umumiy xabar, log da batafsil
console.error(err);
res.status(500).json({ success: false, error: { code: 'SERVER_ERROR', message: 'Serverda xato yuz berdi' } });

Sababi: ichki xato xabarlari dastur tuzilishi haqida ma'lumot beradi — xavfsizlik xatari.


Xulosa

  • Standart format — { success, data, message, meta } — hamma endpoint bir xil
  • Yordamchi funksiyalar — successResponse, errorResponse, notFoundResponse — takrorlanishni kamaytiradi
  • Xato kodlari — NOT_FOUND, UNAUTHORIZED, VALIDATION_ERROR — frontend qaror qilish uchun
  • Global xato handler — Mongoose, JWT xatolarini avtomatik formatlaydi
  • Frontend bir marta yozilgan kod — hamma endpoint uchun ishlaydi

Keyingi dars: 38-dars: Pagination — offset va cursor based