Initial commit
This commit is contained in:
286
skills/typescript-development/reference.md
Normal file
286
skills/typescript-development/reference.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# References
|
||||
|
||||
## Clean Architecture
|
||||
|
||||
### Layer Separation
|
||||
The application follows Clean Architecture with strict dependency rules:
|
||||
|
||||
```
|
||||
Presentation → Application → Domain → Core
|
||||
↓ ↓ ↓
|
||||
Infrastructure Infrastructure (No dependencies)
|
||||
```
|
||||
|
||||
- **Core Layer**: Pure domain entities with no external dependencies
|
||||
- **Domain Layer**: Repository and service interfaces
|
||||
- **Application Layer**: Use cases containing business logic
|
||||
- **Infrastructure Layer**: Concrete implementations (Prisma, external services)
|
||||
- **Presentation Layer**: HTTP controllers, middleware, routes
|
||||
|
||||
### Dependency Injection with inversify
|
||||
```typescript
|
||||
// Required in tsconfig.json
|
||||
{
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
}
|
||||
|
||||
// Required in server.ts (must be first import)
|
||||
import 'reflect-metadata';
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Organization
|
||||
```
|
||||
apps/backend/src/
|
||||
├── application/
|
||||
│ └── property/
|
||||
│ ├── CreatePropertyUseCase.ts
|
||||
│ └── CreatePropertyUseCase.unit.test.ts # Unit tests next to use cases
|
||||
└── __tests__/
|
||||
└── integration/ # Integration tests
|
||||
└── property.integration.test.ts
|
||||
```
|
||||
|
||||
### Mock Factories
|
||||
```typescript
|
||||
import { ILogger } from '../domain/services';
|
||||
|
||||
export function createMockRepository<T>(): jest.Mocked<T> {
|
||||
return {
|
||||
create: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
findByUserId: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn()
|
||||
} as any;
|
||||
}
|
||||
|
||||
export function createMockLogger(): jest.Mocked<ILogger> {
|
||||
return {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Railway Deployment
|
||||
|
||||
### Environment Variables
|
||||
```env
|
||||
# Database (Railway provides this)
|
||||
DATABASE_URL=${{Postgres.DATABASE_URL}}
|
||||
|
||||
# Server
|
||||
PORT=${{PORT}}
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Authentication
|
||||
JWT_SECRET=<generate-secure-random-string>
|
||||
JWT_EXPIRY=7d
|
||||
|
||||
# Frontend (for CORS)
|
||||
FRONTEND_URL=https://your-frontend.railway.app
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
Railway uses Flyway for production migrations. Convert Prisma migrations:
|
||||
|
||||
1. Generate Prisma migration: `npx prisma migrate dev --name feature_name`
|
||||
2. Copy SQL to Flyway format: `migrations/V{number}__{name}.sql`
|
||||
3. Commit both Prisma and Flyway migrations
|
||||
4. Railway runs Flyway on deploy
|
||||
|
||||
|
||||
## API Patterns
|
||||
|
||||
### RESTful Endpoints
|
||||
```
|
||||
GET /api/resources # List with pagination
|
||||
GET /api/resources/:id # Get single resource
|
||||
POST /api/resources # Create new resource
|
||||
PUT /api/resources/:id # Update resource
|
||||
DELETE /api/resources/:id # Delete resource
|
||||
```
|
||||
|
||||
### Response Format
|
||||
```typescript
|
||||
// Success - direct data return
|
||||
res.status(200).json(property);
|
||||
|
||||
// Error - simple error message
|
||||
res.status(400).json({ error: 'Validation failed' });
|
||||
|
||||
// With pagination metadata
|
||||
res.status(200).json({
|
||||
items: properties,
|
||||
total: 100,
|
||||
page: 1,
|
||||
limit: 20
|
||||
});
|
||||
```
|
||||
|
||||
### Pagination
|
||||
```typescript
|
||||
interface PaginationOptions {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
interface PaginatedResult<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrevious: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Security Middleware
|
||||
|
||||
### Essential Security Setup
|
||||
```typescript
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
// Security headers
|
||||
app.use(helmet());
|
||||
|
||||
// CORS
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL,
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100 // limit each IP to 100 requests per windowMs
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// Auth-specific rate limiting
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5,
|
||||
skipSuccessfulRequests: true
|
||||
});
|
||||
app.use('/api/auth/', authLimiter);
|
||||
```
|
||||
|
||||
## Production Logging Strategy
|
||||
|
||||
### Current Implementation (ILogger Interface)
|
||||
```typescript
|
||||
// apps/backend/src/domain/services/ILogger.ts
|
||||
export interface ILogger {
|
||||
info(message: string, context?: object): void;
|
||||
warn(message: string, context?: object): void;
|
||||
error(message: string, context?: object): void;
|
||||
debug(message: string, context?: object): void;
|
||||
}
|
||||
|
||||
// apps/backend/src/infrastructure/services/ConsoleLogger.ts
|
||||
export class ConsoleLogger implements ILogger {
|
||||
info(message: string, context?: object): void {
|
||||
console.log(message, context);
|
||||
}
|
||||
// ... other methods
|
||||
}
|
||||
```
|
||||
|
||||
### Railway Logging Requirements
|
||||
|
||||
For production deployment to Railway, you need:
|
||||
- **Structured JSON output** - Railway aggregates JSON logs
|
||||
- **Stdout/stderr** - Railway captures console output
|
||||
- **No file logging** - Containers are ephemeral
|
||||
- **Request correlation** - Track requests across services
|
||||
- **Performance metrics** - Identify slow operations
|
||||
|
||||
### Recommended Logging Solutions
|
||||
|
||||
**Option 1: Pino (Recommended)**
|
||||
- Fastest JSON logger
|
||||
- Built for cloud deployments
|
||||
- Low overhead
|
||||
|
||||
```bash
|
||||
npm install pino pino-pretty
|
||||
```
|
||||
|
||||
**Option 2: Winston**
|
||||
- More features
|
||||
- Heavier, more complex
|
||||
|
||||
**Option 3: Enhanced ConsoleLogger**
|
||||
- Keep it simple with structured JSON
|
||||
|
||||
```typescript
|
||||
export class StructuredConsoleLogger implements ILogger {
|
||||
info(message: string, context?: object): void {
|
||||
console.log(JSON.stringify({
|
||||
level: 'info',
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
...context
|
||||
}));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Request Correlation Pattern
|
||||
```typescript
|
||||
// apps/backend/src/presentation/middleware/requestId.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export function requestIdMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||
const requestId = uuidv4();
|
||||
req.headers['x-request-id'] = requestId;
|
||||
res.setHeader('x-request-id', requestId);
|
||||
next();
|
||||
}
|
||||
|
||||
// Use in use cases:
|
||||
this.logger.info('Creating resource', {
|
||||
requestId: req.headers['x-request-id'],
|
||||
userId: input.userId
|
||||
});
|
||||
```
|
||||
|
||||
### Performance Logging Pattern
|
||||
```typescript
|
||||
async execute(input: CreateResourceInput): Promise<Resource> {
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const resource = await this.repository.create(input);
|
||||
|
||||
const duration = Date.now() - start;
|
||||
this.logger.info('Resource created', { resourceId: resource.id, duration });
|
||||
|
||||
if (duration > 1000) {
|
||||
this.logger.warn('Slow operation', { operation: 'CreateResource', duration });
|
||||
}
|
||||
|
||||
return resource;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to create resource', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
userId: input.userId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user