Server

Services

Learn how to define reusable business logic with the OpenSya Nest runtime.

Services contain reusable business logic in the OpenSya backend runtime.

They are automatically discovered, compiled and registered from the server/services directory.

Controllers should stay thin and delegate application logic to services.

Services are the preferred place for business logic in OpenSya applications.

Creating a Service

A service is created using the global defineService helper.

server/services/candidate/find-by-id.ts
const findById = async (candidateId: string) => {
  const Candidate = getModel('Candidate');

  return Candidate.findById(candidateId);
};

export default defineService(findById);

The service handler can receive parameters and return synchronous or asynchronous values.

Service Handler

A service is simply a function registered by the OpenSya runtime.

type DefineService = <P extends any[], R>(
  handler: (...args: P) => MayBePromise<R>,
) => {
  handler: (...args: P) => MayBePromise<R>;
};

This means services can accept any number of arguments.

server/services/candidate/search.ts
type SearchCandidateInput = {
  query?: string;
  available?: boolean;
};

const search = async (input: SearchCandidateInput) => {
  const Candidate = getModel('Candidate');

  return Candidate.find({
    ...(input.available !== undefined ? { available: input.available } : {}),
    ...(input.query
      ? {
          $or: [
            { firstName: new RegExp(input.query, 'i') },
            { lastName: new RegExp(input.query, 'i') },
            { email: new RegExp(input.query, 'i') },
          ],
        }
      : {}),
  });
};

export default defineService(search);

Calling a Service

Services are accessed through the OpenSya runtime using useService.

server/controllers/candidates/[id]/index.get.ts
export default defineController(async ({ req }) => {
  return useService('candidate.findById')(req.params.id);
});

The service name is derived from its file path.

Service Name
server/services/candidate/find-by-id.ts
candidate.findById
Services are registered in the runtime registry and can be accessed by name using useService().

File-based Naming

OpenSya infers service names from the filesystem.

Structure
server/services/
├── candidate/
│   ├── find-by-id.ts
│   ├── search.ts
│   └── create.ts
└── interview/
    └── schedule.ts

Generated service names:

FileService Name
candidate/find-by-id.tscandidate.findById
candidate/search.tscandidate.search
candidate/create.tscandidate.create
interview/schedule.tsinterview.schedule

Typed Services

Services can be explicitly typed using regular TypeScript types.

server/services/interview/schedule.ts
type ScheduleInterviewInput = {
  candidateId: string;
  interviewerId: string;
  scheduledAt: Date;
};

type ScheduleInterviewResult = {
  id: string;
  candidateId: string;
  interviewerId: string;
  scheduledAt: Date;
};

const scheduleInterview = async (
  input: ScheduleInterviewInput,
): Promise<ScheduleInterviewResult> => {
  const Interview = getModel('Interview');

  const interview = await Interview.create({
    candidateId: input.candidateId,
    interviewerId: input.interviewerId,
    scheduledAt: input.scheduledAt,
  });

  return {
    id: interview.id,
    candidateId: input.candidateId,
    interviewerId: input.interviewerId,
    scheduledAt: input.scheduledAt,
  };
};

export default defineService(scheduleInterview);

Then it can be called from a controller.

server/controllers/interviews/index.post.ts
export default defineController(async ({ req }) => {
  return useService('interview.schedule')(req.body);
});

Services and Models

Services are a good place to interact with database models.

server/services/candidate/create.ts
type CreateCandidateInput = {
  firstName: string;
  lastName: string;
  email: string;
};

const createCandidate = async (input: CreateCandidateInput) => {
  const Candidate = getModel('Candidate');

  return Candidate.create({
    firstName: input.firstName,
    lastName: input.lastName,
    email: input.email,
  });
};

export default defineService(createCandidate);

This keeps controllers focused on HTTP concerns and moves database logic into reusable services.

Services Calling Other Services

Services can also call other services through useService.

server/services/candidate/archive.ts
const archiveCandidate = async (candidateId: string) => {
  const candidate = await useService('candidate.findById')(candidateId);

  if (!candidate) {
    throw new Error('Candidate not found');
  }

  candidate.archived = true;

  return candidate.save();
};

export default defineService(archiveCandidate);
Avoid circular service dependencies.If two services depend on each other, consider extracting shared logic into a third service.

Runtime Discovery

During startup, OpenSya automatically:

  1. Discovers service files
  2. Infers service names from file paths
  3. Registers services in the runtime registry
  4. Generates typings
  5. Makes services available through useService()
Runtime Lifecycle
Filesystem Discovery
Service Compilation
Service Registration
Type Generation
Runtime Execution

Best Practices

Keep services focused and reusable.

A service should usually represent one business action.

Good examples include:

  • candidate.create
  • candidate.findById
  • candidate.search
  • interview.schedule
  • application.submit

Avoid putting HTTP-specific logic inside services.

Controllers should handle request input and response concerns, while services handle business behavior.

Services should not depend on Express-specific objects such as req or res unless absolutely necessary.

Philosophy

Services are the business layer of the OpenSya backend runtime.

They make recruitment and business infrastructures easier to maintain by centralizing reusable logic and keeping controllers small.

Together with controllers, models and guards, services form the core execution layer of the generated NestJS runtime.