DA
Tüm yazılara dön
API Tasarımında RESTful'dan GraphQL'e: Hangi Yaklaşımı Ne Zaman Kullanmalı?

API Tasarımında RESTful'dan GraphQL'e: Hangi Yaklaşımı Ne Zaman Kullanmalı?

API TasarımıRESTGraphQLgRPCBackend Geliştirme

API Tasarımında RESTful'dan GraphQL'e: Hangi Yaklaşımı Ne Zaman Kullanmalı?

Modern yazılım geliştirmede API tasarımı, uygulamanızın başarısını doğrudan etkileyen kritik kararlardan biridir. REST'ten GraphQL'e, gRPC'den WebSocket'lere kadar birçok seçenek var. Bu yazıda, on yılı aşkın API geliştirme deneyimime dayanarak, her yaklaşımın güçlü ve zayıf yönlerini, ne zaman hangisini kullanmanız gerektiğini detaylıca inceleyeceğim.

API Tasarımının Temel Prensipleri

Hangi teknolojiyi seçerseniz seçin, her iyi API tasarımının altında yatan temel prensipler vardır:

1. Tutarlılık (Consistency)

API'niz boyunca tutarlı naming, response formatları ve davranışlar kullanın:

// Tutarlı naming convention
GET /api/users          // Kullanıcıları listele
GET /api/users/123      // Belirli kullanıcıyı getir
POST /api/users         // Yeni kullanıcı oluştur
PUT /api/users/123      // Kullanıcıyı güncelle
DELETE /api/users/123   // Kullanıcıyı sil

// Tutarlı response format
{
  "data": { ... },
  "meta": {
    "timestamp": "2025-05-26T10:00:00Z",
    "version": "1.0"
  },
  "errors": null
}

2. Öngörülebilirlik (Predictability)

Geliştiriciler API'nizin nasıl davranacağını tahmin edebilmeli:

// Öngörülebilir HTTP status kodları
200 OK           // Başarılı GET, PUT
201 Created      // Başarılı POST
204 No Content   // Başarılı DELETE
400 Bad Request  // Client hatası
401 Unauthorized // Authentication gerekli
404 Not Found    // Kaynak bulunamadı
500 Server Error // Server hatası

3. Geliştiriciler için Kolay Kullanım

API'niz geliştiricilerin işini zorlaştırmamalı, kolaylaştırmalı:

// İyi: Anlamlı hata mesajları
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email adresi geçersiz format",
    "field": "email",
    "value": "invalid-email"
  }
}

// Kötü: Belirsiz hata mesajları
{
  "error": "Bad request"
}

REST API: Zaman Testi Geçen Klasik

REST hâlâ API dünyasının kralı ve bunun geçerli sebepleri var.

REST'in Güçlü Yönleri

1. Basitlik ve Anlaşılabilirlik

// Express.js ile basit REST endpoint
app.get("/api/users/:id", async (req, res) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) {
      return res.status(404).json({
        error: "User not found",
      });
    }
    res.json({ data: user });
  } catch (error) {
    res.status(500).json({
      error: "Internal server error",
    });
  }
});

app.post("/api/users", async (req, res) => {
  try {
    const user = await User.create(req.body);
    res.status(201).json({ data: user });
  } catch (error) {
    if (error.name === "ValidationError") {
      return res.status(400).json({
        error: "Validation failed",
        details: error.errors,
      });
    }
    res.status(500).json({
      error: "Internal server error",
    });
  }
});

2. HTTP Altyapısıyla Uyum

REST, HTTP'nin tüm özelliklerini kullanır:

  • Caching (ETags, Cache-Control)
  • Authentication (Bearer tokens, cookies)
  • Content negotiation (Accept headers)

3. Araç Ekosistemi

REST için olgun araçlar ve kütüphaneler:

  • Swagger/OpenAPI dokümantasyon
  • Postman test koleksiyonları
  • API gateway'ler

REST'in Sınırlamaları

1. Over-fetching ve Under-fetching

// Over-fetching örneği: Sadece isim lazım ama tüm user objesi dönüyor
GET /api/users/123
{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "address": { ... },
  "preferences": { ... },
  "lastLoginHistory": [ ... ] // Gereksiz data
}

// Under-fetching örneği: User'ın post'larını ayrı istek yaparak almak zorunda
GET /api/users/123        // 1. request
GET /api/users/123/posts  // 2. request

2. API Versioning Zorlukları

// URL-based versioning
/api/v1/users
/api/v2/users

// Header-based versioning
Accept: application/vnd.myapi.v2+json

// Her ikisi de backward compatibility sorunları yaratabilir

REST Ne Zaman Ideal?

  • CRUD operasyonları ağırlıkta olan uygulamalar
  • Microservice mimarisi (her servis kendi REST API'si)
  • Caching'in kritik olduğu sistemler
  • Takım REST konusunda deneyimli ve öğrenme eğrisi düşük olmalı

GraphQL: Esnek ve Güçlü Alternatif

Facebook'un geliştirdiği GraphQL, REST'in bazı sınırlamalarını çözmek için tasarlandı.

GraphQL'in Güçlü Yönleri

1. Flexible Data Fetching

# Client tam olarak neye ihtiyacı varsa onu alabilir
query GetUserProfile {
  user(id: 123) {
    name
    email
    posts(limit: 5) {
      title
      createdAt
      comments(limit: 3) {
        content
        author {
          name
        }
      }
    }
  }
}

2. Strong Type System

# Schema definition
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  createdAt: DateTime!
}

type Query {
  user(id: ID!): User
  posts(limit: Int, offset: Int): [Post!]!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
}

3. Real-time Subscriptions

# WebSocket tabanlı real-time updates
subscription OnCommentAdded($postId: ID!) {
  commentAdded(postId: $postId) {
    id
    content
    author {
      name
    }
    createdAt
  }
}

GraphQL Implementation Örneği

// Apollo Server ile GraphQL API
const { ApolloServer, gql } = require("apollo-server-express");

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  type Query {
    user(id: ID!): User
    users: [User!]!
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }
`;

const resolvers = {
  Query: {
    user: async (parent, { id }) => {
      return await User.findById(id);
    },
    users: async () => {
      return await User.findAll();
    },
  },

  Mutation: {
    createUser: async (parent, { name, email }) => {
      return await User.create({ name, email });
    },
    createPost: async (parent, { title, content, authorId }) => {
      return await Post.create({ title, content, authorId });
    },
  },

  User: {
    posts: async (user) => {
      return await Post.findByAuthorId(user.id);
    },
  },

  Post: {
    author: async (post) => {
      return await User.findById(post.authorId);
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });

GraphQL'in Zorlukları

1. Caching Karmaşıklığı

// HTTP caching REST ile kolay
GET /api/users/123 -> Cache key: "users-123"

// GraphQL'de query-specific caching gerekli
query GetUser {
  user(id: 123) {
    name
    email
  }
}
// Her farklı query için farklı cache stratejisi

2. N+1 Problem

// Dikkat: N+1 query problemi
const resolvers = {
  User: {
    // Her user için ayrı veritabanı sorgusu (YANLIŞ!)
    posts: async (user) => {
      return await Post.findByAuthorId(user.id); // N+1 problem
    },
  },
};

// Çözüm: DataLoader kullanımı
const DataLoader = require("dataloader");

const postLoader = new DataLoader(async (userIds) => {
  const posts = await Post.findByAuthorIds(userIds);
  return userIds.map((id) => posts.filter((post) => post.authorId === id));
});

const resolvers = {
  User: {
    posts: async (user) => {
      return await postLoader.load(user.id); // Batch loading
    },
  },
};

GraphQL Ne Zaman İdeal?

  • Frontend ihtiyaçları çeşitli ve değişken
  • Mobile uygulamalar (bandwidth optimization)
  • Rapid prototyping gereken projeler
  • Complex data relationships var

gRPC: Yüksek Performans İçin

Google'ın geliştirdiği gRPC, özellikle microservice'ler arası iletişim için tasarlandı.

gRPC'nin Avantajları

1. Protocol Buffers ile Type Safety

// user.proto
syntax = "proto3";

package user;

service UserService {
  rpc GetUser(GetUserRequest) returns (UserResponse);
  rpc CreateUser(CreateUserRequest) returns (UserResponse);
  rpc UpdateUser(UpdateUserRequest) returns (UserResponse);
  rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  repeated string roles = 4;
  int64 created_at = 5;
}

message GetUserRequest {
  int32 id = 1;
}

message UserResponse {
  User user = 1;
  bool success = 2;
  string error = 3;
}

2. Streaming Support

// Node.js gRPC server streaming örneği
function getUsers(call) {
  const users = getUsersFromDatabase();

  users.forEach((user) => {
    call.write({ user: user });
  });

  call.end();
}

function watchUserUpdates(call) {
  const userId = call.request.userId;

  // Real-time user updates stream
  const subscription = userUpdateEmitter.on("userUpdate", (update) => {
    if (update.userId === userId) {
      call.write({ update: update });
    }
  });

  call.on("cancelled", () => {
    subscription.unsubscribe();
  });
}

3. Code Generation

# Protocol buffer'dan otomatik kod üretimi
protoc --js_out=import_style=commonjs,binary:./generated \
       --grpc_out=grpc_js:./generated \
       --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
       user.proto

gRPC'nin Sınırlamaları

  • Browser support kısıtlı (grpc-web proxy gerekli)
  • Human-readable değil (debugging zorluğu)
  • Load balancer compatibility issues

gRPC Ne Zaman İdeal?

  • Microservice'ler arası internal iletişim
  • Yüksek performans gereksinimi
  • Strong typing kritik
  • Streaming ihtiyacı var

WebSocket ve Server-Sent Events: Real-Time İletişim

WebSocket: İki Yönlü Real-Time

// WebSocket server (Node.js + ws)
const WebSocket = require("ws");

const wss = new WebSocket.Server({ port: 8080 });

wss.on("connection", (ws, req) => {
  console.log("Client connected");

  // Client'dan mesaj geldiğinde
  ws.on("message", (message) => {
    const data = JSON.parse(message);

    switch (data.type) {
      case "join_room":
        // Room'a katıl
        ws.room = data.room;
        ws.send(
          JSON.stringify({
            type: "joined",
            room: data.room,
          })
        );
        break;

      case "chat_message":
        // Room'daki tüm kullanıcılara broadcast
        wss.clients.forEach((client) => {
          if (client.room === ws.room && client !== ws) {
            client.send(
              JSON.stringify({
                type: "chat_message",
                message: data.message,
                sender: data.sender,
              })
            );
          }
        });
        break;
    }
  });

  ws.on("close", () => {
    console.log("Client disconnected");
  });
});

// Client tarafı
const socket = new WebSocket("ws://localhost:8080");

socket.onopen = () => {
  // Room'a katıl
  socket.send(
    JSON.stringify({
      type: "join_room",
      room: "general",
    })
  );
};

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);

  switch (data.type) {
    case "chat_message":
      displayMessage(data.message, data.sender);
      break;
  }
};

// Mesaj gönder
function sendMessage(message) {
  socket.send(
    JSON.stringify({
      type: "chat_message",
      message: message,
      sender: "user123",
    })
  );
}

Server-Sent Events: Tek Yönlü Real-Time

// SSE endpoint
app.get("/api/events", (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
    "Access-Control-Allow-Origin": "*",
  });

  // Heartbeat
  const heartbeat = setInterval(() => {
    res.write('data: {"type":"heartbeat"}\n\n');
  }, 30000);

  // Notification listener
  const notificationListener = (notification) => {
    res.write(`data: ${JSON.stringify(notification)}\n\n`);
  };

  notificationEmitter.on("notification", notificationListener);

  req.on("close", () => {
    clearInterval(heartbeat);
    notificationEmitter.off("notification", notificationListener);
  });
});

// Client tarafı
const eventSource = new EventSource("/api/events");

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.type === "notification") {
    showNotification(data.message);
  }
};

Hibrit Yaklaşımlar: En İyi Her İkisinden

Gerçek dünya uygulamalarında genellikle birden fazla API türü birlikte kullanılır.

REST + GraphQL Hibrit

// Express.js app'de hem REST hem GraphQL endpoint'leri
const express = require("express");
const { ApolloServer } = require("apollo-server-express");

const app = express();

// REST endpoints (public API, third-party integrations)
app.get("/api/v1/users", getUsersREST);
app.post("/api/v1/users", createUserREST);

// GraphQL endpoint (internal frontend)
const server = new ApolloServer({ typeDefs, resolvers });
server.applyMiddleware({ app, path: "/graphql" });

// WebSocket (real-time features)
const httpServer = http.createServer(app);
const wsServer = new WebSocket.Server({ server: httpServer });

wsServer.on("connection", handleWebSocketConnection);

httpServer.listen(4000);

BFF (Backend for Frontend) Pattern

// Mobile BFF - GraphQL optimized for mobile
const mobileBFF = new ApolloServer({
  typeDefs: mobileTypeDefs,
  resolvers: mobileResolvers,
  plugins: [
    // Mobile-specific optimizations
    responseCachePlugin({
      ttl: 300, // 5 minute cache for mobile
    }),
  ],
});

// Web BFF - REST API optimized for web
const webBFF = express();
webBFF.use("/api", webRouter);

// Admin BFF - gRPC for internal tools
const adminService = new grpc.Server();
adminService.addService(AdminServiceDefinition, adminServiceImplementation);

API Güvenliği: Tüm Yaklaşımlar İçin Kritik

Authentication ve Authorization

// JWT-based authentication middleware
const jwt = require("jsonwebtoken");

function authenticateToken(req, res, next) {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) {
    return res.status(401).json({ error: "Access token required" });
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: "Invalid token" });
    }
    req.user = user;
    next();
  });
}

// Role-based authorization
function requireRole(role) {
  return (req, res, next) => {
    if (!req.user.roles.includes(role)) {
      return res.status(403).json({ error: "Insufficient privileges" });
    }
    next();
  };
}

// Usage
app.get(
  "/api/admin/users",
  authenticateToken,
  requireRole("admin"),
  getAdminUsers
);

Rate Limiting

const rateLimit = require("express-rate-limit");

// General rate limiting
const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: {
    error: "Too many requests, please try again later",
  },
});

// Strict rate limiting for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // Only 5 login attempts per 15 minutes
  skipSuccessfulRequests: true,
});

app.use("/api/", generalLimiter);
app.use("/api/auth/", authLimiter);

Input Validation

const Joi = require("joi");

// Schema validation
const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  password: Joi.string()
    .min(8)
    .pattern(new RegExp("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])"))
    .required(),
  age: Joi.number().integer().min(18).max(120),
});

function validateUser(req, res, next) {
  const { error } = userSchema.validate(req.body);

  if (error) {
    return res.status(400).json({
      error: "Validation failed",
      details: error.details.map((detail) => ({
        field: detail.path.join("."),
        message: detail.message,
      })),
    });
  }

  next();
}

app.post("/api/users", validateUser, createUser);

API Monitoring ve Analytics

Metrics Collection

// Prometheus metrics collection
const promClient = require("prom-client");

// Request duration histogram
const httpRequestDuration = new promClient.Histogram({
  name: "http_request_duration_seconds",
  help: "Duration of HTTP requests in seconds",
  labelNames: ["method", "route", "status_code"],
});

// Request counter
const httpRequestCount = new promClient.Counter({
  name: "http_requests_total",
  help: "Total number of HTTP requests",
  labelNames: ["method", "route", "status_code"],
});

// Middleware to collect metrics
function metricsMiddleware(req, res, next) {
  const start = Date.now();

  res.on("finish", () => {
    const duration = (Date.now() - start) / 1000;
    const route = req.route ? req.route.path : req.path;

    httpRequestDuration
      .labels(req.method, route, res.statusCode)
      .observe(duration);

    httpRequestCount.labels(req.method, route, res.statusCode).inc();
  });

  next();
}

app.use(metricsMiddleware);

// Metrics endpoint
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", promClient.register.contentType);
  res.end(await promClient.register.metrics());
});

Error Tracking

// Structured error logging
const winston = require("winston");

const logger = winston.createLogger({
  level: "info",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: "error.log", level: "error" }),
    new winston.transports.File({ filename: "combined.log" }),
  ],
});

// Error handling middleware
function errorHandler(err, req, res, next) {
  // Log error with context
  logger.error("API Error", {
    error: err.message,
    stack: err.stack,
    method: req.method,
    url: req.url,
    userId: req.user?.id,
    requestId: req.id,
    userAgent: req.get("User-Agent"),
    ip: req.ip,
  });

  // Don't leak error details in production
  const isDevelopment = process.env.NODE_ENV === "development";

  res.status(err.status || 500).json({
    error: isDevelopment ? err.message : "Internal server error",
    ...(isDevelopment && { stack: err.stack }),
  });
}

app.use(errorHandler);

Karar Matrisi: Hangi API Tipini Seçmeli?

| Kriter | REST | GraphQL | gRPC | WebSocket | | ------------------- | -------- | -------- | -------- | --------- | | Öğrenme Eğrisi | Düşük | Orta | Yüksek | Orta | | Performans | İyi | Orta | Mükemmel | Mükemmel | | Caching | Mükemmel | Zor | Orta | N/A | | Browser Support | Mükemmel | Mükemmel | Kısıtlı | İyi | | Real-time | Hayır | Sınırlı | İyi | Mükemmel | | Type Safety | Hayır | İyi | Mükemmel | Hayır | | Flexibility | Düşük | Yüksek | Düşük | Yüksek |

Sonuç ve Öneriler

REST Kullanın Eğer:

  • Basit CRUD operasyonları yapıyorsanız
  • HTTP caching kritik ise
  • Third-party developer'lar API'nizi kullanacaksa
  • Takımınız REST deneyimli ve proje süresi kısıtlı ise

GraphQL Kullanın Eğer:

  • Frontend ihtiyaçları çeşitli ve değişken ise
  • Mobile performansı kritik ise
  • Rapid development gerekli ise
  • Complex data relationships var ise

gRPC Kullanın Eğer:

  • Microservice'ler arası iletişim yapıyorsanız
  • Yüksek performans gerekli ise
  • Strong typing kritik ise
  • Browser support gerekli değil ise

WebSocket/SSE Kullanın Eğer:

  • Real-time features gerekli ise
  • Chat, notifications, live updates var ise
  • Server'dan client'a sürekli data push gerekli ise

Hibrit Yaklaşım Kullanın Eğer:

  • Farklı client türleri var ise (mobile, web, admin)
  • Hem real-time hem traditional features gerekli ise
  • Legacy sistem entegrasyonu var ise

En önemli nokta: Doğmatik olmayın. Her proje kendine özgü ihtiyaçlara sahiptir. Teknoloji seçimini iş gereksinimlerinize, takım yeteneklerinize ve proje kısıtlarınıza göre yapın. Çoğu zaman en iyi çözüm, birden fazla yaklaşımı birleştiren hibrit bir mimaridir.

API tasarımı bir maraton, sprint değil. Başlangıçta basit tutun, gereksinimler netleştikçe evrim geçirin. Ve en önemlisi: API'nizi kullanan geliştiricilerin deneyimini her zaman öncelikleyin.