TypeScript ile Node.js Authentication API Geliştirme: Kapsamlı Rehber
Giriş
Modern web uygulamaları geliştirirken tip güvenliği ve kod kalitesi giderek daha önemli hale geliyor. Bu eğitimde, TypeScript’in sunduğu güçlü özellikleri gerçek bir proje üzerinde öğreneceğiz. JWT ve Google OAuth kimlik doğrulaması içeren bir Todo uygulaması geliştirirken, TypeScript’in temel kavramlarını ve best practice’lerini uygulayacağız. Uygulamanın tüm haline GitHub repository adresinden ulaşabilirsiniz.
Bu proje size şunları kazandıracak:
- TypeScript ile güvenli kod yazma pratiği
- Modern bir REST API tasarlama deneyimi
- Authentication ve Authorization implementasyonu
- MongoDB ile TypeScript kullanımı
- Clean Architecture prensiplerini uygulama
TypeScript Özellikleri ve Proje Yapısı
Bu projede TypeScript’in temel özelliklerini kullanarak güvenli ve ölçeklenebilir bir API geliştireceğiz. Her özelliği gerçek proje kodlarımızdan örneklerle açıklayalım:
1. TypeScript Temelleri (Basics)
TypeScript’in temel yapı taşlarını projemizde şu şekilde kullanıyoruz:
// src/config/env.ts'de Tip Tanımlamaları
const PORT: number = Number(process.env.PORT) || 3000;
const JWT_EXPIRES_IN: string = '1d';
// src/middleware/auth.middleware.ts'de Type Assertion
const decoded = jwt.verify(token, JWT_SECRET) as IJwtPayload;
// Burada JWT'den gelen veriyi IJwtPayload tipine dönüştürüyoruz
// src/interfaces/user.interface.ts'de Literal Types
type UserRole = 'user' | 'admin';
// User modelinde kullanıcı rollerini sadece bu iki değerle sınırlıyoruz
Projedeki Kullanım Örnekleri:
PORT
tanımısrc/index.ts
‘de server başlatırken kullanılıyor- Type assertion
auth.middleware.ts
‘de JWT doğrulamasında kullanılıyor - UserRole tipi
IUser
interface’inde kullanıcı rolünü kısıtlamak için kullanılıyor
2. Fonksiyonlar (Functions)
TypeScript’te fonksiyonları tip güvenli şekilde tanımlıyoruz:
// src/services/auth.service.ts'de Method Signatures
interface IAuthService {
login(credentials: IUserLogin): Promise<{ user: IUser; token: string }>;
register(userData: IUserRegistration): Promise<IUser>;
}
// src/controllers/auth.controller.ts'de Implementation
public async login(req: Request, res: Response): Promise<void> {
const { email, password } = req.body;
const result = await this.authService.login({ email, password });
// ...
}
Projedeki Kullanım Örnekleri:
IAuthService
interface’iauth.service.ts
‘de servis implementasyonunu tanımlıyor- Controller’lardaki tüm handler fonksiyonları Request ve Response tiplerini kullanıyor
- Tüm async fonksiyonlar Promise return type’ı ile tanımlanıyor
3. Nesne Tipleri (Object Types)
Projemizde karmaşık veri yapılarını nesne tipleriyle modelliyoruz:
// src/config/database.ts'de Configuration Types
type DatabaseConfig = {
uri: string;
options: {
useNewUrlParser: boolean;
useUnifiedTopology: boolean;
};
};
// src/controllers/todo.controller.ts'de Request Types
interface ITodoCreate {
title: string;
description?: string; // Optional property örneği
}
Projedeki Kullanım Örnekleri:
DatabaseConfig
tipi MongoDB bağlantı ayarlarını tanımlıyorITodoCreate
interface’i todo oluşturma endpoint’inde request body validasyonu için kullanılıyor- Optional property’ler todo güncellemelerinde kısmi güncellemeye izin veriyor
4. Interfaces
Interface’leri projemizde hem tip tanımı hem de sözleşme olarak kullanıyoruz:
// src/interfaces/base.interface.ts'de Base Interface
interface IBaseEntity {
_id: string;
createdAt: Date;
updatedAt: Date;
}
// src/interfaces/user.interface.ts'de Interface Extension
interface IUser extends IBaseEntity {
email: string;
password?: string;
name: string;
role: UserRole;
}
Projedeki Kullanım Örnekleri:
IBaseEntity
tüm MongoDB modellerimizin temel alanlarını tanımlıyorIUser
interface’i User modelinin şemasını ve metodlarını tanımlıyor- Interface’ler mongoose model tanımlarında tip güvenliği sağlıyor
5. TypeScript Compiler
TypeScript derleyicisini projemiz için özel olarak yapılandırıyoruz:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
Compiler Ayarlarının Önemi:
strict
: Katı tip kontrolü sağlıyortarget
: Modern JavaScript özelliklerini kullanmamızı sağlıyormodule
: Node.js ile uyumlu modül sistemi kullanıyoruz
6. Classes
OOP prensiplerini TypeScript classes ile uyguluyoruz:
// src/services/base.service.ts'de Abstract Base Class
abstract class BaseService<T extends IBaseEntity> {
constructor(protected model: Model<T>) {}
abstract create(data: Partial<T>): Promise<T>;
async findById(id: string): Promise<T | null> {
return this.model.findById(id);
}
}
// src/services/todo.service.ts'de Class Implementation
class TodoService extends BaseService<ITodo> {
async create(data: ICreateTodo): Promise<ITodo> {
return this.model.create(data);
}
async markAsCompleted(id: string): Promise<ITodo | null> {
return this.model.findByIdAndUpdate(id, { completed: true });
}
}
Neden Bu Özellik?
- Abstract class’lar ile ortak davranışları zorunlu kılıyoruz
- Inheritance ile kod tekrarını önlüyoruz
- Service katmanını organize ediyoruz
Projedeki Kullanım Örnekleri:
BaseService
tüm servisler için temel CRUD operasyonlarını tanımlıyorTodoService
veAuthService
bu base class’ı extend ederek kendi özel metodlarını ekliyor- Abstract metodlar sayesinde her servisin kendi create metodunu implemente etmesi zorunlu
7. Generics
Generic tipleri projemizde şu şekilde kullanıyoruz:
// src/services/base.service.ts'de Generic Service
class CrudService<T extends IBaseEntity> {
async findOne(filter: FilterQuery<T>): Promise<T | null> {
return this.model.findOne(filter);
}
}
// src/utils/response.ts'de Generic Response Handler
function createResponse<T>(success: boolean, message: string, data?: T): IApiResponse<T> {
return { success, message, data };
}
// src/utils/error.ts'de Generic Error Handler
class ApiError<T = unknown> extends Error {
constructor(public statusCode: number, message: string, public data?: T) {
super(message);
}
}
Neden Bu Özellik?
- Tip güvenliğini koruyarak yeniden kullanılabilir kod yazıyoruz
- Farklı veri tipleriyle çalışabilen fonksiyonlar oluşturuyoruz
- Tip parametreleri ile esnek yapılar kuruyoruz
Projedeki Kullanım Örnekleri:
CrudService
farklı model tipleriyle çalışabiliyor (User, Todo vb.)createResponse
her türlü veri yapısı için tutarlı API yanıtları oluşturuyorApiError
farklı hata tipleri için özelleştirilebilir error handling sağlıyor
8. Type Narrowing
Runtime’da tip kontrolü ve daraltma işlemlerini güvenli şekilde yapıyoruz:
// src/utils/error.ts'de Type Guards
function isError(error: unknown): error is Error {
return error instanceof Error;
}
// src/middleware/error.middleware.ts'de Error Handling
function handleError(error: unknown): IApiResponse<null> {
if (isError(error)) {
return createResponse(false, error.message);
}
if (typeof error === 'string') {
return createResponse(false, error);
}
return createResponse(false, 'Unknown error occurred');
}
// src/types/error.types.ts'de Discriminated Unions
type ValidationError = {
type: 'validation';
fields: { [key: string]: string };
};
type AuthError = {
type: 'auth';
message: string;
};
type AppError = ValidationError | AuthError;
// src/utils/error-handler.ts'de Error Type Handling
function handleAppError(error: AppError) {
switch (error.type) {
case 'validation':
return error.fields;
case 'auth':
return error.message;
}
}
Neden Bu Özellik?
- Runtime’da tip güvenliğini sağlıyoruz
- Hata yönetimini daha güvenli yapıyoruz
- Union type’ları doğru şekilde işliyoruz
Projedeki Kullanım Örnekleri:
isError
type guard’ı middleware’lerde hata tipini doğru şekilde belirlememizi sağlıyor- Error handling middleware’de farklı hata tiplerini ayırt edebiliyoruz
- Discriminated union’lar ile validation ve auth hatalarını ayrı ayrı işleyebiliyoruz
Proje Özeti
Geliştireceğimiz API aşağıdaki özellikleri içerecek:
Kullanıcı Yönetimi
- Kayıt ve Giriş
- JWT Authentication
- Google OAuth Entegrasyonu
- Rol tabanlı yetkilendirme
Todo İşlemleri
- Todo oluşturma, okuma, güncelleme, silme
- Kullanıcıya özel todo’lar
- Todo durumu değiştirme
Güvenlik ve Validasyon
- Input validasyonu
- Route koruması
- Error handling
Proje Yapısı
Projemiz şu şekilde organize edilmiştir:
src/
├── config/ # Konfigürasyon dosyaları
├── controllers/ # HTTP request handlers
├── interfaces/ # TypeScript interfaces
├── middleware/ # Express middleware
├── models/ # Mongoose modelleri
├── routes/ # API routes
├── services/ # İş mantığı
├── utils/ # Yardımcı fonksiyonlar
└── index.ts # Uygulama giriş noktası
TypeScript ile Proje Geliştirme
1. Proje Kurulumu ve TypeScript Konfigürasyonu
İlk adım olarak TypeScript’i projemize entegre edelim:
mkdir nodejs-typescript-auth
cd nodejs-typescript-auth
npm init -y
npm install typescript ts-node @types/node --save-dev
Bağımlılıklar
Projemiz için gerekli paketleri yükleyelim:
# Ana bağımlılıklar
npm install express mongoose dotenv jsonwebtoken bcrypt passport passport-google-oauth20 passport-jwt cors
# Tip tanımlamaları
npm install @types/express @types/mongoose @types/jsonwebtoken @types/bcrypt @types/passport @types/passport-google-oauth20 @types/passport-jwt @types/cors --save-dev
TypeScript Konfigürasyonu
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
2. Veri Modellerini Tanımlama
User Modeli
TypeScript interface’leri ile kullanıcı modelimizi tanımlayalım:
// src/interfaces/user.interface.ts
// Base interface - temel kullanıcı özellikleri
interface IBaseUser {
email: string;
name: string;
}
// Ana kullanıcı interface'i - tüm özellikleri içerir
interface IUser extends IBaseUser {
password?: string; // Optional: Google OAuth kullanıcıları için şifre olmayabilir
googleId?: string; // Optional: Sadece Google ile giriş yapanlar için
role: 'user' | 'admin'; // Union type ile rol kısıtlaması
comparePassword(candidatePassword: string): Promise<boolean>;
}
// Kayıt için gerekli alanlar
interface IUserRegistration {
email: string;
password: string;
name: string;
}
// Giriş için gerekli alanlar
interface IUserLogin {
email: string;
password: string;
}
Todo Modeli
Todo işlemleri için gerekli interface’leri tanımlayalım:
// src/interfaces/todo.interface.ts
interface ITodo {
title: string;
description?: string;
completed: boolean;
user: string; // Referans: User ID
createdAt: Date;
updatedAt: Date;
}
// Todo oluşturma için gerekli alanlar
interface ICreateTodo {
title: string;
description?: string;
}
// Todo güncelleme için opsiyonel alanlar
interface IUpdateTodo {
title?: string;
description?: string;
completed?: boolean;
}
3. Servis Katmanı Implementasyonu
Base Service
Generic bir base service oluşturarak kod tekrarını önleyelim:
// src/services/base.service.ts
abstract class BaseService<T> {
constructor(protected model: Model<T>) {}
async findById(id: string): Promise<T | null> {
return this.model.findById(id);
}
async findOne(filter: FilterQuery<T>): Promise<T | null> {
return this.model.findOne(filter);
}
async find(filter: FilterQuery<T>): Promise<T[]> {
return this.model.find(filter);
}
}
Auth Service
Kimlik doğrulama işlemlerini yöneten servis:
// src/services/auth.service.ts
class AuthService extends BaseService<IUser> {
public async register(userData: IUserRegistration): Promise<IUser> {
const existingUser = await this.findOne({ email: userData.email });
if (existingUser) {
throw new Error('Bu email adresi zaten kullanımda');
}
const user = await this.model.create(userData);
return user;
}
public async login(loginData: IUserLogin): Promise<{ user: IUser; token: string }> {
const user = await this.findOne({ email: loginData.email });
if (!user || !(await user.comparePassword(loginData.password))) {
throw new Error('Geçersiz kimlik bilgileri');
}
return {
user,
token: this.generateToken(user),
};
}
private generateToken(user: IUser): string {
return jwt.sign({ id: user._id, email: user.email, role: user.role }, process.env.JWT_SECRET!, { expiresIn: '1d' });
}
}
Todo Service
Todo işlemlerini yöneten servis:
// src/services/todo.service.ts
class TodoService extends BaseService<ITodo> {
public async getAllTodos(userId: string): Promise<ITodo[]> {
return this.find({ user: userId });
}
public async createTodo(todoData: ICreateTodo, userId: string): Promise<ITodo> {
return this.model.create({
...todoData,
user: userId,
completed: false,
});
}
public async updateTodo(todoId: string, todoData: Partial<ITodo>, userId: string): Promise<ITodo | null> {
return this.model.findOneAndUpdate({ _id: todoId, user: userId }, todoData, { new: true });
}
}
4. Middleware Implementasyonu
TypeScript ile güvenli middleware yazımı:
// src/middleware/auth.middleware.ts
// Request tipini genişletme
declare global {
namespace Express {
interface Request {
user?: IUser;
}
}
}
export const isAuthenticated = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
throw new Error('Token bulunamadı');
}
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as IJwtPayload;
const user = await UserModel.findById(decoded.id);
if (!user) {
throw new Error('Kullanıcı bulunamadı');
}
req.user = user;
next();
} catch (error) {
res.status(401).json({
success: false,
message: 'Yetkilendirme hatası',
error: error instanceof Error ? error.message : 'Bilinmeyen hata',
});
}
};
5. Controller Katmanı
TypeScript ile tip güvenli controller’lar:
// src/controllers/todo.controller.ts
class TodoController {
constructor(private todoService: TodoService) {}
public async getAllTodos(req: Request, res: Response): Promise<void> {
try {
const todos = await this.todoService.getAllTodos(req.user!._id);
res.status(200).json({
success: true,
message: 'Todolar başarıyla getirildi',
data: todos,
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Todolar getirilirken hata oluştu',
error: error instanceof Error ? error.message : 'Bilinmeyen hata',
});
}
}
}
API Endpoint’leri
Auth Endpoints
POST /api/auth/register
- Request Body: { email: string, password: string, name: string }
- Response: { success: boolean, message: string, data: { user: IUser, token: string } }
POST /api/auth/login
- Request Body: { email: string, password: string }
- Response: { success: boolean, message: string, data: { user: IUser, token: string } }
GET /api/auth/google
- Google OAuth başlatma endpoint'i
GET /api/auth/google/callback
- Google OAuth callback endpoint'i
Todo Endpoints
GET /api/todos
- Headers: { Authorization: "Bearer ${token}" }
- Response: { success: boolean, message: string, data: ITodo[] }
POST /api/todos
- Headers: { Authorization: "Bearer ${token}" }
- Request Body: { title: string, description?: string }
- Response: { success: boolean, message: string, data: ITodo }
PUT /api/todos/:id
- Headers: { Authorization: "Bearer ${token}" }
- Request Body: { title?: string, description?: string, completed?: boolean }
- Response: { success: boolean, message: string, data: ITodo }
DELETE /api/todos/:id
- Headers: { Authorization: "Bearer ${token}" }
- Response: { success: boolean, message: string }
Best Practices
Tip Güvenliği
- Her zaman spesifik tipler kullanın
any
tipinden kaçının- Union types ile değer kümelerini sınırlayın
- Generic tipler ile yeniden kullanılabilir kod yazın
Kod Organizasyonu
- Her bir katman için ayrı klasör kullanın
- Interface’leri ilgili domain klasöründe tutun
- Servis katmanını abstract class’lar ile genelleştirin
Error Handling
- Custom error sınıfları oluşturun
- Global error handler kullanın
- Hata mesajlarını standardize edin
Güvenlik
- Hassas bilgileri environment variable’larda saklayın
- Input validasyonu yapın
- Rate limiting uygulayın
- CORS politikalarını doğru yapılandırın
Sonuç
Bu projede:
- TypeScript’in tip sistemi ile güvenli kod yazmayı
- OOP prensiplerini TypeScript ile uygulamayı
- Modern bir API mimarisi tasarlamayı
- Authentication ve authorization implementasyonunu öğrendik.
TypeScript, projemize:
- Derleme zamanında hata yakalama
- Daha iyi IDE desteği
- Self-documenting kod
- Maintainability gibi önemli avantajlar sağladı.