Claude
Skills
Sign in
Back

typescript-development

Included with Lifetime
$97 forever

Helps build and extend TypeScript Express APIs using Clean Architecture, inversify dependency injection, Prisma ORM, and Railway deployment patterns established in the upkeep-io project.

General

What this skill does


# TypeScript Development

## Research Protocol

**MANDATORY:** Follow the research protocol in `@shared/research-protocol.md` before implementing backend features.

### When to Research

You MUST use `mcp__Ref__ref_search_documentation` before:
- Using Prisma features you haven't verified this session
- Implementing inversify patterns
- Using Express middleware patterns
- Making Zod validation decisions
- Advising on JWT or authentication patterns

**Never assume training data reflects current library versions. When in doubt, verify.**

## Project Context

This is a **monorepo** property management system with shared libraries:

```
upkeep-io/
├── apps/
│   ├── backend/              # Node/Express API (CommonJS)
│   └── frontend/             # Vue 3 SPA (ES Modules)
└── libs/                     # Shared libraries
    ├── domain/               # Entities, errors (Property, MaintenanceWork, User)
    ├── validators/           # Zod schemas (shared validation)
    └── auth/                 # JWT utilities
```

**Key Principle:** Backend and frontend share validation schemas and domain entities from `libs/` for maximum code reuse.

## Capabilities

- Build new features following Clean Architecture with inversify DI
- Implement JWT + bcrypt authentication
- Create comprehensive unit tests with mocked repositories
- Set up production logging for Railway deployment
- Configure Prisma repositories with type transformations
- Implement shared validation using Zod schemas

## Creating a New Feature

Follow this 8-step workflow that matches the actual project structure:

### 1. Define Domain Entity (if needed)

```typescript
// libs/domain/src/entities/Resource.ts
export interface CreateResourceData {
  userId: string;
  name: string;
  description?: string;
}

export interface Resource extends CreateResourceData {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}
```

### 2. Create Validation Schema

```typescript
// libs/validators/src/resource.ts
import { z } from 'zod';

export const createResourceSchema = z.object({
  userId: z.string().uuid(),
  name: z.string().min(1).max(255),
  description: z.string().max(1000).optional()
});

export type CreateResourceInput = z.infer<typeof createResourceSchema>;
```

### 3. Create Repository Interface

```typescript
// apps/backend/src/domain/repositories/IResourceRepository.ts
import { Resource, CreateResourceData } from '@domain/entities';

export interface IResourceRepository {
  create(data: CreateResourceData): Promise<Resource>;
  findById(id: string): Promise<Resource | null>;
  findByUserId(userId: string): Promise<Resource[]>;
  update(id: string, data: Partial<Resource>): Promise<Resource>;
  delete(id: string): Promise<void>;
}
```

### 4. Implement Use Case

```typescript
// apps/backend/src/application/resource/CreateResourceUseCase.ts
import { injectable, inject } from 'inversify';
import { IResourceRepository } from '../../domain/repositories';
import { ILogger } from '../../domain/services';
import { ValidationError } from '@domain/errors';
import { createResourceSchema } from '@validators/resource';
import { Resource } from '@domain/entities';

interface CreateResourceInput {
  userId: string;
  name: string;
  description?: string;
}

@injectable()
export class CreateResourceUseCase {
  constructor(
    @inject('IResourceRepository') private repository: IResourceRepository,
    @inject('ILogger') private logger: ILogger
  ) {}

  async execute(input: CreateResourceInput): Promise<Resource> {
    // Validate with shared schema
    const validation = createResourceSchema.safeParse(input);
    if (!validation.success) {
      throw new ValidationError(validation.error.errors[0].message);
    }

    // Execute business logic
    const resource = await this.repository.create(validation.data);

    this.logger.info('Resource created', { resourceId: resource.id, userId: input.userId });

    return resource;
  }
}
```

### 5. Create Prisma Repository

```typescript
// apps/backend/src/infrastructure/repositories/PrismaResourceRepository.ts
import { injectable } from 'inversify';
import { PrismaClient } from '@prisma/client';
import { IResourceRepository } from '../../domain/repositories';
import { Resource, CreateResourceData } from '@domain/entities';

@injectable()
export class PrismaResourceRepository implements IResourceRepository {
  private prisma: PrismaClient;

  constructor() {
    this.prisma = new PrismaClient();
  }

  async create(data: CreateResourceData): Promise<Resource> {
    const result = await this.prisma.resource.create({ data });

    // Transform Prisma nulls to undefined for domain entity
    return {
      ...result,
      description: result.description ?? undefined
    };
  }

  async findById(id: string): Promise<Resource | null> {
    const result = await this.prisma.resource.findUnique({ where: { id } });
    if (!result) return null;

    return {
      ...result,
      description: result.description ?? undefined
    };
  }

  async findByUserId(userId: string): Promise<Resource[]> {
    const results = await this.prisma.resource.findMany({
      where: { userId },
      orderBy: { createdAt: 'desc' }
    });

    return results.map(r => ({
      ...r,
      description: r.description ?? undefined
    }));
  }

  async update(id: string, data: Partial<Resource>): Promise<Resource> {
    const result = await this.prisma.resource.update({ where: { id }, data });

    return {
      ...result,
      description: result.description ?? undefined
    };
  }

  async delete(id: string): Promise<void> {
    await this.prisma.resource.delete({ where: { id } });
  }
}
```

### 6. Register in Container

```typescript
// apps/backend/src/container.ts
import { IResourceRepository } from './domain/repositories';
import { PrismaResourceRepository } from './infrastructure/repositories';
import { CreateResourceUseCase } from './application/resource';
import { ResourceController } from './presentation/controllers';

export function createContainer(): Container {
  const container = new Container();

  // ... existing bindings ...

  // Repository
  container
    .bind<IResourceRepository>('IResourceRepository')
    .to(PrismaResourceRepository)
    .inTransientScope();

  // Use Case
  container.bind(CreateResourceUseCase).toSelf().inTransientScope();

  // Controller
  container.bind(ResourceController).toSelf().inTransientScope();

  return container;
}
```

### 7. Create Controller

```typescript
// apps/backend/src/presentation/controllers/ResourceController.ts
import { injectable, inject } from 'inversify';
import { Response, NextFunction } from 'express';
import { CreateResourceUseCase } from '../../application/resource';
import { AuthRequest } from '../middleware';

@injectable()
export class ResourceController {
  constructor(
    @inject(CreateResourceUseCase) private createUseCase: CreateResourceUseCase
  ) {}

  async create(req: AuthRequest, res: Response, next: NextFunction): Promise<void> {
    try {
      if (!req.user) {
        res.status(401).json({ error: 'Unauthorized' });
        return;
      }

      const resource = await this.createUseCase.execute({
        ...req.body,
        userId: req.user.userId
      });

      res.status(201).json(resource);
    } catch (error) {
      next(error);
    }
  }
}
```

### 8. Write Unit Tests

```typescript
// apps/backend/src/application/resource/CreateResourceUseCase.unit.test.ts
import { CreateResourceUseCase } from './CreateResourceUseCase';
import { IResourceRepository } from '../../domain/repositories';
import { ILogger } from '../../domain/services';
import { ValidationError } from '@domain/errors';
import { Resource } from '@domain/entities';

describe('CreateResourceUseCase', () => {
  let useCase: CreateResourceUseCase;
  let mockRepository: jest.Mocked<IResourceRepository>;
  let mockLogger: jest.Mocked<ILogger>;

  beforeEach(() => {
    mockRepository = {
      create: jest.fn(),
      findById: jest.fn(),
  

Related in General