Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions docs/API_VERSIONING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# API Versioning Strategy

## Overview

The StellarTip API implements URI-based versioning to ensure backward compatibility and enable future API evolution without breaking existing clients.

## Versioning Scheme

### URI-Based Versioning

All API endpoints are prefixed with a version identifier in the URL path:

```
https://api.stellartip.com/v1/{endpoint}
```

**Example:**
- Versioned endpoint: `GET /v1/auth/login`
- Unversioned (backward compatible): `GET /auth/login` → defaults to v1

### Current Version: v1.0.0

The current stable version is `v1.0.0`. All business logic endpoints are versioned under `/v1`.

## Implementation Details

### NestJS Configuration

API versioning is configured in `src/main.ts` using NestJS's built-in versioning support:

```typescript
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
```

The `defaultVersion: '1'` setting ensures backward compatibility - requests without a version prefix automatically default to v1.

### Controller Versioning

Business logic controllers are decorated with `@Version('1')` to specify their API version:

```typescript
@ApiTags('auth')
@Controller('auth')
@Version('1')
export class AuthController {
// ...
}
```

### Infrastructure Endpoints (Not Versioned)

The following endpoints are NOT versioned as they are infrastructure-related:
- **Health endpoints**: `/health/*` - Health checks and monitoring
- **Root endpoint**: `/` - API welcome message
- **Swagger docs**: `/api/docs` - API documentation

These remain accessible without version prefix and are excluded from versioning.

## Backward Compatibility

### Default Version Behavior

The API uses `defaultVersion: '1'` which provides seamless backward compatibility:

- **Versioned requests**: `GET /v1/auth/login` → Works as expected
- **Unversioned requests**: `GET /auth/login` → Automatically treated as v1 (no redirect needed)

This approach is simpler and more efficient than using redirects, as it avoids the overhead of HTTP redirects while maintaining full backward compatibility.

## Migration Guide for Clients

### Recommended Approach

1. **Update your base URL:**
```javascript
// Old (still works due to backward compatibility)
const BASE_URL = 'https://api.stellartip.com';

// New (recommended)
const BASE_URL = 'https://api.stellartip.com/v1';
```

2. **Test your application:** Both versioned and unversioned endpoints should work identically.

3. **Monitor usage:** Track which version your clients are using to plan future deprecations.

### Timeline

- **Phase 1 (Current):** Both versioned (`/v1/*`) and unversioned endpoints work. Unversioned requests automatically default to v1.
- **Phase 2 (Future):** Unversioned endpoints will be deprecated. Clients will receive deprecation warnings.
- **Phase 3 (Future):** Unversioned endpoints will be removed. Only `/v1` endpoints will be supported.

## Versioning Best Practices

### When to Create a New Version

Create a new API version when:

1. **Breaking Changes:** Any change that breaks existing client functionality
- Removing or renaming fields
- Changing data types
- Modifying required parameters
- Changing authentication mechanisms

2. **Major Behavioral Changes:** Significant changes in how endpoints work
- Different response formats
- Changed business logic
- Altered error handling

### Non-Breaking Changes (No New Version Needed)

These changes can be made within the current version:

- Adding new optional fields
- Adding new endpoints
- Bug fixes that don't change behavior
- Performance improvements
- Documentation updates

### Future Versioning

When v2 is needed:

1. Create new controller classes or methods with `@Version('2')`
2. Maintain v1 controllers for backward compatibility
3. Update documentation to highlight differences
4. Set a deprecation timeline for v1

## Testing Versioned Endpoints

### Using cURL

```bash
# Versioned endpoint (recommended)
curl https://api.stellartip.com/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password"}'

# Unversioned (backward compatible, defaults to v1)
curl https://api.stellartip.com/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password"}'

# Infrastructure endpoint (not versioned)
curl https://api.stellartip.com/health
```

### Using Swagger Documentation

The Swagger UI is available at `/api/docs` and automatically reflects the versioned endpoints. All examples in the documentation use the `/v1` prefix for business logic endpoints.

## Monitoring and Logging

### Version Metrics

The API should track:
- Request counts per version (v1 vs unversioned)
- Error rates per version
- Response times per version

This data helps determine when to deprecate older versions.

### Deprecation Warnings

When a version is deprecated, responses will include:

```http
HTTP/1.1 200 OK
X-API-Deprecation: true
X-API-Sunset-Date: 2025-12-31
X-API-Recommended-Version: v2
```

## Support and Questions

For questions about API versioning or migration assistance:
- Check the Swagger documentation at `/api/docs`
- Review this document for common scenarios
- Contact the development team for specific concerns

## Changelog

### v1.0.0 (Current)
- Initial API versioning implementation
- All business logic endpoints moved to `/v1` prefix
- Infrastructure endpoints (health, root, docs) remain unversioned
- Backward compatibility maintained through defaultVersion setting
- Swagger documentation updated with versioning information
9 changes: 8 additions & 1 deletion src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiOperation, ApiTags, ApiResponse } from '@nestjs/swagger';
import { AppService } from './app.service';

@ApiTags('app')
Expand All @@ -8,6 +8,13 @@ export class AppController {
constructor(private readonly appService: AppService) {}

@ApiOperation({ summary: 'API welcome message' })
@ApiResponse({
status: 200,
description: 'Welcome message retrieved successfully',
schema: {
example: 'Welcome to StellarTip-Backend API!',
},
})
@Get()
getHello(): string {
return this.appService.getHello();
Expand Down
3 changes: 2 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@
},
],
})
export class AppModule {}
export class AppModule implements NestModule {
configure(consumer: Mi

Check failure on line 59 in src/app.module.ts

View workflow job for this annotation

GitHub Actions / TypeScript Check

')' expected.
113 changes: 112 additions & 1 deletion src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
Req,
HttpException,
HttpStatus,
Version,
} from '@nestjs/common';
import { Request } from 'express';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiResponse } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { SignupDto } from './dto/signup.dto';
import { LoginDto } from './dto/login.dto';
Expand All @@ -20,10 +21,39 @@ import { User } from '../entities/user.entity';

@ApiTags('auth')
@Controller('auth')
@Version('1')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@ApiOperation({ summary: 'Login with Stellar wallet address' })
@ApiBody({
schema: {
type: 'object',
properties: {
walletAddress: {
type: 'string',
example: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ',
},
},
required: ['walletAddress'],
},
})
@ApiResponse({
status: 200,
description: 'Successfully authenticated with Stellar wallet',
schema: {
example: {
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
expires_in: 3600,
user: {
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'johndoe',
email: 'user@example.com',
},
},
},
})
@Post('stellar/login')
@AuthThrottle()
async loginStellar(@Body('walletAddress') walletAddress: string): Promise<{
Expand All @@ -50,6 +80,16 @@ export class AuthController {
}

@ApiOperation({ summary: 'Get authentication nonce for Stellar wallet' })
@ApiResponse({
status: 200,
description: 'Nonce generated successfully',
schema: {
example: {
nonce: 'abc123def456',
message: 'Please sign this message to authenticate with StellarTip-Backend. Nonce: abc123def456 Timestamp: 2024-01-01T00:00:00Z',
},
},
})
@Get('nonce')
@AuthThrottle()
getNonce(@Query('walletAddress') walletAddress: string): {
Expand All @@ -66,6 +106,23 @@ export class AuthController {
}

@ApiOperation({ summary: 'Create a new account' })
@ApiResponse({
status: 201,
description: 'Account created successfully',
schema: {
example: {
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
expires_in: 3600,
user: {
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'johndoe',
email: 'user@example.com',
displayName: 'John Doe',
},
},
},
})
@Post('signup')
@AuthThrottle()
async signup(@Body() signupDto: SignupDto): Promise<{
Expand All @@ -83,6 +140,22 @@ export class AuthController {
}

@ApiOperation({ summary: 'Login with email and password' })
@ApiResponse({
status: 200,
description: 'Successfully logged in',
schema: {
example: {
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
expires_in: 3600,
user: {
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'johndoe',
email: 'user@example.com',
},
},
},
})
@Post('login')
@AuthThrottle()
async login(@Body() loginDto: LoginDto): Promise<{
Expand All @@ -95,6 +168,29 @@ export class AuthController {
}

@ApiOperation({ summary: 'Refresh access token using refresh token' })
@ApiBody({
schema: {
type: 'object',
properties: {
refresh_token: {
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
required: ['refresh_token'],
},
})
@ApiResponse({
status: 200,
description: 'Token refreshed successfully',
schema: {
example: {
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
expires_in: 3600,
},
},
})
@Post('refresh')
async refresh(@Body('refresh_token') refreshToken: string): Promise<{
access_token: string;
Expand All @@ -111,6 +207,21 @@ export class AuthController {
}

@ApiOperation({ summary: 'Get current user profile from JWT' })
@ApiResponse({
status: 200,
description: 'User profile retrieved successfully',
schema: {
example: {
id: '123e4567-e89b-12d3-a456-426614174000',
username: 'johndoe',
email: 'user@example.com',
displayName: 'John Doe',
bio: 'Content creator and developer',
avatarUrl: 'https://example.com/avatar.jpg',
createdAt: '2024-01-01T00:00:00Z',
},
},
})
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('profile')
Expand Down
Loading
Loading