Bir mobil uygulama, SPA veya ucuncu parti entegrasyonu icin veri katmani olusturmak istiyorsaniz, REST API bilmek zorunlu. Bu rehberde Express.js kullanarak profesyonel seviyede bir urun API'si insa edeceksiniz — pagination, validation ve hata yonetimi dahil.
Bu Rehberde Ne Ogreneceksiniz
- REST mimarisinin temel prensipleri ve HTTP method'larinin dogru kullanimi
- Express.js ile modular router yapisi kurma
- 5 CRUD endpoint'i yazma (listeleme, detay, olusturma, guncelleme, silme)
- Input validation middleware'i ile guvenli veri kabulu
- Merkezi hata yonetimi (custom error class + error middleware)
- Offset-based pagination ve standart response wrapper
- curl ile tum endpoint'leri test etme
Adim 1: REST API Nedir?
REST (Representational State Transfer), istemci-sunucu iletisimini standartlastiran bir mimari yaklasimdir. Dort temel prensibi vardir:
Kaynak tabanli URL'ler. Her endpoint bir kaynak (resource) temsil eder. /api/v1/products urunler koleksiyonunu, /api/v1/products/42 ise tekil bir urunu isaret eder. Fiil URL'de bulunmaz — fiil HTTP method'undan gelir.
Durumsuzluk (Stateless). Sunucu, istemcinin onceki isteklerini hatirlamaz. Her istek kendi basina yeterli bilgiyi tasir. Bu, yatay olceklemeyi kolaylastirir: herhangi bir sunucu herhangi bir istegi isleyebilir.
JSON veri formati. Istek ve yanit govdeleri JSON formatindadir. Content-Type: application/json header'i bunu belirtir.
HTTP method'lari anlam tasir. Her method farkli bir isleme karsilik gelir:
| Method | Kullanim | Idempotent? | Body Var mi? |
|---|---|---|---|
| GET | Kaynak oku | Evet | Hayir |
| POST | Yeni kaynak olustur | Hayir | Evet |
| PUT | Kaynak guncelle (tam veya kismi) | Evet | Evet |
| DELETE | Kaynak sil | Evet | Hayir |
| PATCH | Kaynak kismi guncelle | Hayir | Evet |
PUT vs PATCH farki nedir? PUT, kaynagi tamamen degistirir — gonderilmeyen alanlar sifirlanir. PATCH, sadece gonderilen alanlari gunceller. Pratikte cogu API, PUT'u PATCH gibi kullanir (partial update). Bu rehberde PUT ile partial update yapacagiz cunku istemci tarafinda daha yaygin tercih edilen yaklasim budur.
Adim 2: Proje Kurulumu
Terminal'i acin ve proje klasorunu olusturun:
mkdir product-api && cd product-api
npm init -y
npm install express cors uuid
Uc paket kurduk:
- express — HTTP sunucu ve routing
- cors — Cross-Origin isteklere izin vermek icin
- uuid — Benzersiz urun ID'leri uretmek icin
Proje yapisini olusturun:
mkdir -p routes middleware
touch index.js routes/products.js middleware/validate.js middleware/errorHandler.js
Son gorunum:
product-api/
├── index.js
├── routes/
│ └── products.js
├── middleware/
│ ├── validate.js
│ └── errorHandler.js
├── package.json
└── node_modules/
Adim 3: Ana Sunucu ve Router Yapisi
index.js dosyasini acin ve sunucu iskeletini yazin:
const express = require('express');
const cors = require('cors');
const productRoutes = require('./routes/products');
const { errorHandler } = require('./middleware/errorHandler');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/v1/products', productRoutes);
// 404 handler — tanimlanmamis rotalar icin
app.use((req, res) => {
res.status(404).json({ error: 'Endpoint bulunamadi' });
});
// Merkezi hata yonetimi — en sona yazilir
app.use(errorHandler);
app.listen(PORT, () => {
console.log(`API sunucusu http://localhost:${PORT} adresinde calisiyor`);
});
Dikkat edilecek noktalar:
express.json()gelen JSON body'leri otomatik parse eder.- Tum urun rotalarini
/api/v1/productsprefix'i altina bagladik. Versiyon prefix'i (v1) ileride breaking change yaptiginizda eski istemcileri kirmadan yeni birv2acmanizi saglar. - 404 handler, tanimli olmayan tum rotalari yakalar.
errorHandleren sonda tanimlanir — Express'te hata middleware'i 4 parametre alir (err, req, res, next), bu yuzden sirasi onemlidir.
Adim 4: CRUD Endpoint'leri
routes/products.js dosyasini acin. Oncelikle in-memory veri deposu ve yardimci fonksiyonlari tanimlayalim, ardindan bes endpoint'i tek tek yazalim.
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { validateProduct } = require('../middleware/validate');
const router = express.Router();
// In-memory veri deposu
let products = [
{
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'Mekanik Klavye',
price: 1250,
category: 'Elektronik',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
name: 'Ergonomik Mouse',
price: 680,
category: 'Elektronik',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
// GET /api/v1/products — Tum urunleri listele (pagination destekli)
router.get('/', (req, res) => {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 10));
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedProducts = products.slice(startIndex, endIndex);
const totalPages = Math.ceil(products.length / limit);
res.json({
data: paginatedProducts,
pagination: {
currentPage: page,
totalPages,
totalItems: products.length,
itemsPerPage: limit,
hasNextPage: page < totalPages,
hasPrevPage: page > 1,
},
});
});
// GET /api/v1/products/:id — Tek urun getir
router.get('/:id', (req, res, next) => {
const product = products.find((p) => p.id === req.params.id);
if (!product) {
const err = new Error('Urun bulunamadi');
err.statusCode = 404;
return next(err);
}
res.json({ data: product });
});
// POST /api/v1/products — Yeni urun olustur
router.post('/', validateProduct, (req, res) => {
const { name, price, category } = req.body;
const newProduct = {
id: uuidv4(),
name,
price,
category: category || 'Genel',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
products.push(newProduct);
res.status(201).json({ data: newProduct });
});
// PUT /api/v1/products/:id — Urun guncelle (partial update)
router.put('/:id', (req, res, next) => {
const index = products.findIndex((p) => p.id === req.params.id);
if (index === -1) {
const err = new Error('Urun bulunamadi');
err.statusCode = 404;
return next(err);
}
const { name, price, category } = req.body;
// Sadece gonderilen alanlari guncelle
if (name !== undefined) products[index].name = name;
if (price !== undefined) {
if (typeof price !== 'number' || price <= 0) {
const err = new Error('Fiyat sifirdan buyuk bir sayi olmali');
err.statusCode = 400;
return next(err);
}
products[index].price = price;
}
if (category !== undefined) products[index].category = category;
products[index].updatedAt = new Date().toISOString();
res.json({ data: products[index] });
});
// DELETE /api/v1/products/:id — Urun sil
router.delete('/:id', (req, res, next) => {
const index = products.findIndex((p) => p.id === req.params.id);
if (index === -1) {
const err = new Error('Urun bulunamadi');
err.statusCode = 404;
return next(err);
}
products.splice(index, 1);
res.status(204).send();
});
module.exports = router;
Her endpoint'in islevini ozetleyelim:
- GET / —
pagevelimitquery parametreleri ile sayfalama yapar.limit1-100 arasinda sinirlidir — istemcinin tek istekte 10.000 kayit cekmesini engeller. - GET /:id — Urun bulunamazsa
next(err)ile merkezi hata handler'a yonlendirir. - POST / —
validateProductmiddleware'i gecerse yeni urun olusturur ve201 Createddondurur. - PUT /:id — Sadece gonderilen alanlari gunceller.
updatedAtzaman damgasini otomatik yeniler. - DELETE /:id — Basarili silmede
204 No Contentdondurur — yanit govdesi bos kalir.
Adim 5: Input Validation
Disaridan gelen her veri guvenilmezdir. middleware/validate.js dosyasini acin:
function validateProduct(req, res, next) {
const errors = [];
const { name, price } = req.body;
// name kontrolu
if (!name || typeof name !== 'string') {
errors.push('name alani zorunlu ve metin olmali');
} else if (name.trim().length < 2) {
errors.push('name en az 2 karakter olmali');
} else if (name.trim().length > 200) {
errors.push('name en fazla 200 karakter olmali');
}
// price kontrolu
if (price === undefined || price === null) {
errors.push('price alani zorunlu');
} else if (typeof price !== 'number' || price <= 0) {
errors.push('price sifirdan buyuk bir sayi olmali');
} else if (price > 1_000_000) {
errors.push('price 1.000.000 degerini asamaz');
}
if (errors.length > 0) {
return res.status(400).json({ error: 'Validation hatasi', details: errors });
}
// Temizlenmis degerleri body'ye geri yaz
req.body.name = name.trim();
next();
}
module.exports = { validateProduct };
Validation middleware'i su isleri yapar:
- Zorunlu alanlarin varligini kontrol eder.
- Tip kontrolu yapar (string mi, number mi).
- Sinir kontrolleri uygular (min/max uzunluk, min/max deger).
- Hata varsa
400 Bad Requestile detayli hata listesi dondurur. - Hata yoksa
next()cagirarak istegi bir sonraki handler'a iletir.
Adim 6: Merkezi Hata Yonetimi
Her route'ta try-catch yazmak yerine merkezi bir hata middleware'i kullanacagiz. middleware/errorHandler.js dosyasini acin:
function errorHandler(err, req, res, _next) {
const statusCode = err.statusCode || 500;
const isServerError = statusCode >= 500;
// Sunucu hatalarini logla — istemciye detay gosterme
if (isServerError) {
console.error(`[${new Date().toISOString()}] ${err.stack || err.message}`);
}
res.status(statusCode).json({
error: isServerError ? 'Sunucu hatasi' : err.message,
...(process.env.NODE_ENV === 'development' && isServerError
? { stack: err.stack }
: {}),
});
}
module.exports = { errorHandler };
Hata yonetimi neden middleware olmali? Uc sebebi var. Birincisi, kod tekrarini onler — her route'ta ayni try-catch blogunu yazmak zorunda kalmazsiniz. Ikincisi, loglama ve hata formatlama mantigi tek noktada toplanir; degistirmek istediginizde tek dosyaya dokunursunuz. Ucuncusu, istemciye giden hata yaniti tutarli olur — bazen JSON bazen duz metin donduren bir API, frontend gelistiriciyi deli eder.
Bu yapida iki onemli ayrim var:
- 4xx hatalari (400, 404, 409) istemcinin hatasidir. Hata mesajini oldugu gibi donduruyoruz.
- 5xx hatalari (500) sunucunun hatasidir. Gercek hata mesajini loglariz ama istemciye sadece "Sunucu hatasi" deriz. Stack trace'i production'da gizleriz, development'ta gosteririz.
Adim 7: Pagination Detaylari
Pagination neden gerekli sorusuyla baslayalim.
Neden pagination gerekli? 10.000 urunluk bir veritabaniniz oldugunu dusunun. Tek istekte hepsini gondermek: (1) sunucudan 5-10 MB JSON cikisi, (2) istemcide 2-3 saniye parse suresi, (3) mobilde bant genisligi israfi, (4) veritabaninda gereksiz yuk. Pagination bu sorunlarin hepsini cozer — istemci sadece ihtiyaci olan dilimi alir.
Listeleme endpoint'indeki pagination kodunu tekrar inceleyelim:
// Sayfa numarasi: minimum 1
const page = Math.max(1, parseInt(req.query.page) || 1);
// Sayfa basi oge: minimum 1, maksimum 100
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 10));
// Dizi dilimleme
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedProducts = products.slice(startIndex, endIndex);
// Toplam sayfa sayisi
const totalPages = Math.ceil(products.length / limit);
Yanit yapisi su sekildedir:
{
"data": [
{ "id": "...", "name": "Mekanik Klavye", "price": 1250 }
],
"pagination": {
"currentPage": 1,
"totalPages": 1,
"totalItems": 2,
"itemsPerPage": 10,
"hasNextPage": false,
"hasPrevPage": false
}
}
hasNextPage ve hasPrevPage boolean alanlari, istemcinin "Sonraki" ve "Onceki" butonlarini gosterip gostermeyecegine karar vermesini kolaylastirir.
Adim 8: HTTP Durum Kodlari
API'nizin dogru durum kodlarini dondurmesi, istemci gelistiricinin hayatini kolaylastirir. Kodunuzda kullandiginiz ve karsilasacaginiz durum kodlari:
| Kod | Isim | Ne Zaman Kullanilir |
|---|---|---|
| 200 | OK | Basarili GET, PUT istekleri |
| 201 | Created | POST ile yeni kaynak olusturuldu |
| 204 | No Content | DELETE basarili, yanit govdesi bos |
| 400 | Bad Request | Validation hatasi, eksik/hatali alan |
| 404 | Not Found | Kaynak bulunamadi |
| 409 | Conflict | Ayni kaynak zaten var (ornegin duplicate name) |
| 500 | Internal Server Error | Sunucu tarafinda beklenmeyen hata |
Bonus: CORS ve Request Logging
Sunucuya iki faydali middleware daha ekleyelim. index.js dosyasinda CORS ayarlarini detaylandirabilirsiniz:
// Detayli CORS ayarlari (index.js'te app.use(cors()) yerine)
app.use(cors({
origin: ['http://localhost:5173', 'https://siteniz.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
Basit bir request logger eklemek icin:
// Request logging middleware — cors ve routes arasina ekleyin
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`
);
});
next();
});
Bu logger her istegin method'unu, URL'ini, durum kodunu ve sure mili saniyesini konsola yazar. Production'da Winston veya Pino gibi yapilandirilmis logger kutuphanelerini tercih edin.
API'yi Test Edin
Sunucuyu baslatin:
node index.js
Simdi baska bir terminal acin ve curl komutlariyla tum endpoint'leri test edin:
# Tum urunleri listele
curl http://localhost:3000/api/v1/products | jq
# Sayfalama ile listele (sayfa 1, sayfa basi 1 urun)
curl "http://localhost:3000/api/v1/products?page=1&limit=1" | jq
# Tek urun getir
curl http://localhost:3000/api/v1/products/550e8400-e29b-41d4-a716-446655440000 | jq
# Yeni urun olustur
curl -X POST http://localhost:3000/api/v1/products \
-H "Content-Type: application/json" \
-d '{"name": "USB-C Hub", "price": 450, "category": "Aksesuar"}' | jq
# Urun guncelle (sadece fiyat)
curl -X PUT http://localhost:3000/api/v1/products/550e8400-e29b-41d4-a716-446655440000 \
-H "Content-Type: application/json" \
-d '{"price": 1100}' | jq
# Urun sil
curl -X DELETE http://localhost:3000/api/v1/products/6ba7b810-9dad-11d1-80b4-00c04fd430c8 -v
# Validation hatasi testi — bos body gonder
curl -X POST http://localhost:3000/api/v1/products \
-H "Content-Type: application/json" \
-d '{}' | jq
# 404 testi — var olmayan ID
curl http://localhost:3000/api/v1/products/yanlis-id | jq
Her komut beklenen durum kodunu ve JSON yanitini dondurmelidir. jq komutu JSON ciktisini okunabilir formatta gosterir — yuklu degilse npm install -g json veya ciktiyi dogrudan okuyabilirsiniz.
Sonraki Adimlar
Bu rehberde in-memory veri deposuyla calistik. Production'a tasimaniz icin su adimlar sirayla gelir:
- Veritabani baglantisi. In-memory dizi sunucu yeniden basladiginda sifirlanir. PostgreSQL (Supabase, Neon), MongoDB veya SQLite kullanarak veriyi kalici yapin.
- Authentication ve JWT. Kimin hangi endpoint'e erisebilecegini kontrol edin.
jsonwebtokenpaketi ile token-based auth ekleyin. POST, PUT, DELETE rotalarini koruma altina alin. - Rate limiting.
express-rate-limitpaketi ile dakika basina istek siniri koyun. API'nizi kotu niyetli kullanim ve DDoS'a karsi korur. - API dokumanasyonu.
swagger-jsdocveswagger-ui-expressile Swagger/OpenAPI dokumani olusturun. Frontend ekibiniz endpoint'leri kesfetsin, test etsin. - Test. Jest veya Vitest ile birim testleri, Supertest ile entegrasyon testleri yazin. Her CRUD endpoint'inin basarili ve hata senaryolarini kapsamali.
API'niz hazir olduktan sonra, kisa URL servisi de eklemek isterseniz Sifirdan URL Kisaltici Yapin rehberimize bakin.
Profesyonel API gelistirme ve yazilim danismanligi icin hizmetlerimizi inceleyin.
Sikca Sorulan Sorular
REST API ile GraphQL arasindaki fark nedir?
REST'te her kaynak icin ayri endpoint tanimlarsiniz: /products, /products/:id, /orders. Istemci hangi alanlari alacagini secemez — sunucu ne dondururse onu alir. GraphQL'de tek endpoint vardir (/graphql) ve istemci sorgusuyla tam olarak hangi alanlari istedigini belirtir. REST, basit CRUD API'ler icin hizli kurulur ve cache mekanizmalari (HTTP cache) dogal olarak calisir. GraphQL, ic ice gecmis veriler ve farkli istemci ihtiyaclari (mobil vs web) olan projelerde parlak.
Bu API'ye authentication nasil eklenir?
En yaygin yontem JWT (JSON Web Token) kullanmaktir. Kullanici giris yaptiginda sunucu bir token uretir, istemci bu token'i her istekte Authorization: Bearer <token> header'inda gonderir. Sunucuda bir auth middleware'i yazarsiniz: token'i dogrular, gecersizse 401 Unauthorized dondurur, gecerliyse req.user'a kullanici bilgisini yazar ve next() cagirir. POST, PUT, DELETE rotalarinin onune bu middleware'i koyarsiniz.
In-memory veri yerine veritabani nasil kullanilir?
products dizisini bir veritabani tablosuyla degistirin. PostgreSQL icin pg paketi veya Prisma ORM, MongoDB icin mongoose kullanabilirsiniz. Route handler'lardaki products.find(), products.push() gibi dizi islemlerini SQL sorgulari veya ORM cagrilariyla degistirin. Veritabani baglanti bilgilerini .env dosyasinda tutun, dotenv paketi ile yukleyin. Geri kalan yapi — router, middleware, error handler — degismez.
