Services
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.
Creating a Service
A service is created using the global defineService helper.
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.
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.
export default defineController(async ({ req }) => {
return useService('candidate.findById')(req.params.id);
});
The service name is derived from its file path.
server/services/candidate/find-by-id.ts
↓
candidate.findById
useService().File-based Naming
OpenSya infers service names from the filesystem.
server/services/
├── candidate/
│ ├── find-by-id.ts
│ ├── search.ts
│ └── create.ts
└── interview/
└── schedule.ts
Generated service names:
| File | Service Name |
|---|---|
candidate/find-by-id.ts | candidate.findById |
candidate/search.ts | candidate.search |
candidate/create.ts | candidate.create |
interview/schedule.ts | interview.schedule |
Typed Services
Services can be explicitly typed using regular TypeScript types.
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.
export default defineController(async ({ req }) => {
return useService('interview.schedule')(req.body);
});
Services and Models
Services are a good place to interact with database models.
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.
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);
Runtime Discovery
During startup, OpenSya automatically:
- Discovers service files
- Infers service names from file paths
- Registers services in the runtime registry
- Generates typings
- Makes services available through
useService()
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.createcandidate.findByIdcandidate.searchinterview.scheduleapplication.submit
Avoid putting HTTP-specific logic inside services.
Controllers should handle request input and response concerns, while services handle business behavior.
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.