Skip to main content

Overview

The service layer contains the core business logic of Postiz. Services are responsible for data transformation, business rules, and orchestrating interactions between repositories and external services.
Most service logic lives in libraries/nestjs-libraries/src/services/ to enable code sharing between the backend and orchestrator.

Service Organization

libraries/nestjs-libraries/src/services/
├── email.service.ts          # Email sending
├── stripe.service.ts         # Payment processing
├── appsumo.service.ts        # AppSumo integration
└── codes.service.ts          # Promotional codes

apps/backend/src/services/
└── auth/
    ├── auth.service.ts       # Authentication logic
    └── providers/
        └── providers.manager.ts

Basic Service Pattern

Injectable Service

auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service';
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';

@Injectable()
export class AuthService {
  constructor(
    private _userService: UsersService,
    private _organizationService: OrganizationService,
    private _emailService: EmailService,
  ) {}

  async canRegister(provider: string) {
    if (process.env.DISABLE_REGISTRATION !== 'true') {
      return true;
    }
    return (await this._organizationService.getCount()) === 0;
  }

  async routeAuth(
    provider: Provider,
    body: CreateOrgUserDto | LoginUserDto,
    ip: string,
    userAgent: string,
  ) {
    // Business logic here
    if (provider === Provider.LOCAL) {
      const user = await this._userService.getUserByEmail(body.email);
      
      if (body instanceof CreateOrgUserDto) {
        if (user) {
          throw new Error('Email already exists');
        }
        
        const create = await this._organizationService.createOrgAndUser(
          body,
          ip,
          userAgent
        );
        
        return { jwt: await this.jwt(create.users[0].user) };
      }
      
      if (!user || !AuthChecker.comparePassword(body.password, user.password)) {
        throw new Error('Invalid user name or password');
      }
      
      return { jwt: await this.jwt(user) };
    }
    
    // OAuth provider logic
    // ...
  }

  private async jwt(user: User) {
    // JWT generation logic
  }
}

Service Responsibilities

Business Logic

Services contain all business rules and validations:
async createPost(dto: CreatePostDto, userId: string) {
  // Validate business rules
  if (dto.scheduledAt < new Date()) {
    throw new Error('Cannot schedule post in the past');
  }
  
  // Check user permissions
  const canPost = await this._permissionsService.canUserPost(userId);
  if (!canPost) {
    throw new Error('User does not have permission to post');
  }
  
  // Orchestrate multiple operations
  const post = await this._postsRepository.create(dto);
  await this._temporalService.schedulePost(post);
  await this._analyticsService.trackPostCreated(post);
  
  return post;
}

Data Transformation

async getPostWithAnalytics(postId: string) {
  const post = await this._postsRepository.findById(postId);
  const analytics = await this._analyticsRepository.getByPostId(postId);
  
  // Transform data for response
  return {
    id: post.id,
    content: post.content,
    scheduledAt: post.scheduledAt,
    stats: {
      views: analytics.views,
      clicks: analytics.clicks,
      engagement: this.calculateEngagement(analytics),
    },
  };
}

private calculateEngagement(analytics: Analytics): number {
  if (analytics.views === 0) return 0;
  return (analytics.clicks / analytics.views) * 100;
}

Orchestration

Services coordinate between multiple repositories and external services:
async publishPost(postId: string) {
  // Get post and integrations
  const post = await this._postsRepository.findById(postId);
  const integrations = await this._integrationsRepository.findByIds(
    post.integrationIds
  );
  
  // Publish to each integration
  const results = await Promise.allSettled(
    integrations.map(integration => 
      this._integrationManager.publish(integration, post)
    )
  );
  
  // Update post status
  await this._postsRepository.update(postId, {
    status: 'published',
    publishedAt: new Date(),
  });
  
  // Track analytics
  await this._analyticsService.trackPublished(postId, results);
  
  return results;
}

Dependency Injection

Constructor Injection

@Injectable()
export class PostsService {
  constructor(
    private _postsRepository: PostsRepository,
    private _usersService: UsersService,
    private _mediaService: MediaService,
    private _temporalService: TemporalService,
  ) {}
}

Circular Dependencies

Use forwardRef() when services depend on each other:
import { Injectable, forwardRef, Inject } from '@nestjs/common';

@Injectable()
export class PostsService {
  constructor(
    @Inject(forwardRef(() => UsersService))
    private _usersService: UsersService,
  ) {}
}

Repository Pattern

Services interact with data through repositories:
@Injectable()
export class PostsService {
  constructor(
    private _postsRepository: PostsRepository,
  ) {}

  async createPost(dto: CreatePostDto) {
    // Business logic
    const validated = this.validatePost(dto);
    
    // Delegate to repository
    return this._postsRepository.create(validated);
  }

  async findByUser(userId: string) {
    return this._postsRepository.findMany({
      where: { userId },
      include: { media: true },
    });
  }
}
Important: Never access the database directly in services. Always use repository methods.Wrong: this.prisma.post.create(...)
Correct: this._postsRepository.create(...)

External Service Integration

Email Service Example

email.service.ts
import { Injectable } from '@nestjs/common';
import { Resend } from 'resend';
import nodemailer from 'nodemailer';

@Injectable()
export class EmailService {
  private resend?: Resend;
  private transporter?: any;

  constructor() {
    if (process.env.EMAIL_PROVIDER === 'resend') {
      this.resend = new Resend(process.env.RESEND_API_KEY);
    } else {
      this.transporter = nodemailer.createTransport({
        host: process.env.SMTP_HOST,
        port: process.env.SMTP_PORT,
        auth: {
          user: process.env.SMTP_USER,
          pass: process.env.SMTP_PASS,
        },
      });
    }
  }

  hasProvider(): boolean {
    return !!(this.resend || this.transporter);
  }

  async sendEmail(
    to: string,
    subject: string,
    html: string,
    priority: 'top' | 'normal' = 'normal'
  ) {
    if (this.resend) {
      await this.resend.emails.send({
        from: process.env.EMAIL_FROM!,
        to,
        subject,
        html,
      });
    } else if (this.transporter) {
      await this.transporter.sendMail({
        from: process.env.EMAIL_FROM,
        to,
        subject,
        html,
      });
    }
  }
}

Stripe Service Example

stripe.service.ts
import { Injectable } from '@nestjs/common';
import Stripe from 'stripe';

@Injectable()
export class StripeService {
  private stripe: Stripe;

  constructor() {
    this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
      apiVersion: '2023-10-16',
    });
  }

  async createCheckoutSession(
    priceId: string,
    customerId: string,
    metadata: Record<string, string>
  ) {
    return this.stripe.checkout.sessions.create({
      mode: 'subscription',
      customer: customerId,
      line_items: [{ price: priceId, quantity: 1 }],
      success_url: `${process.env.FRONTEND_URL}/success`,
      cancel_url: `${process.env.FRONTEND_URL}/cancel`,
      metadata,
    });
  }

  async cancelSubscription(subscriptionId: string) {
    return this.stripe.subscriptions.cancel(subscriptionId);
  }
}

Error Handling

Custom Exceptions

import { HttpException, HttpStatus } from '@nestjs/common';

class PostNotFoundException extends HttpException {
  constructor(postId: string) {
    super(`Post ${postId} not found`, HttpStatus.NOT_FOUND);
  }
}

@Injectable()
export class PostsService {
  async getPost(postId: string) {
    const post = await this._postsRepository.findById(postId);
    
    if (!post) {
      throw new PostNotFoundException(postId);
    }
    
    return post;
  }
}

Try-Catch Pattern

async publishPost(postId: string) {
  try {
    const post = await this._postsRepository.findById(postId);
    const result = await this._integrationManager.publish(post);
    return result;
  } catch (error) {
    // Log error
    console.error('Failed to publish post:', error);
    
    // Update post status
    await this._postsRepository.update(postId, {
      status: 'failed',
      error: error.message,
    });
    
    // Re-throw or handle
    throw new Error(`Failed to publish post: ${error.message}`);
  }
}

Temporal Integration

Services can trigger Temporal workflows:
import { Injectable } from '@nestjs/common';
import { InjectClient } from 'nestjs-temporal-core';
import { Client } from '@temporalio/client';

@Injectable()
export class PostsService {
  constructor(
    @InjectClient() private readonly temporalClient: Client,
  ) {}

  async schedulePost(post: Post) {
    // Start a Temporal workflow
    await this.temporalClient.workflow.start('publishPost', {
      taskQueue: 'posts',
      workflowId: `post-${post.id}`,
      args: [post.id],
      // Schedule for future execution
      startTime: post.scheduledAt,
    });
  }
}

Testing Services

Unit Test Example

auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';

describe('AuthService', () => {
  let service: AuthService;
  let usersService: UsersService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: UsersService,
          useValue: {
            getUserByEmail: jest.fn(),
            create: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<AuthService>(AuthService);
    usersService = module.get<UsersService>(UsersService);
  });

  it('should register a new user', async () => {
    const dto = { email: '[email protected]', password: 'password123' };
    
    jest.spyOn(usersService, 'getUserByEmail').mockResolvedValue(null);
    jest.spyOn(usersService, 'create').mockResolvedValue({ id: '1', ...dto });

    const result = await service.register(dto);
    expect(result).toHaveProperty('jwt');
  });
});

Best Practices

1

Single Responsibility

Each service should have a single, well-defined responsibility.
2

Use Repositories

Always access data through repository methods, never directly via Prisma.
3

Keep Services Pure

Avoid HTTP-specific logic (req, res objects). That belongs in controllers.
4

Handle Errors

Use try-catch blocks and throw meaningful exceptions.
5

Inject Dependencies

Use constructor injection for all dependencies.
6

Write Tests

Services should be thoroughly unit tested.

Next Steps

Database Schema

Learn about the database structure and Prisma

Creating Integrations

Build a new social media integration

Build docs developers (and LLMs) love