@geekmidas/services
v0.0.1
Published
Service discovery and dependency injection system for TypeScript applications with full type safety and automatic lifecycle management.
Downloads
255
Readme
@geekmidas/services
Service discovery and dependency injection system for TypeScript applications with full type safety and automatic lifecycle management.
Features
- Type-Safe Services: Full TypeScript support with generic type inference
- Lazy Initialization: Services are initialized only when needed
- Singleton Pattern: Services are cached and reused across requests
- Dependency Injection: Automatic service resolution and injection
- Environment Integration: Seamless integration with @geekmidas/envkit
- Service Discovery: Centralized service registry and discovery
- Error Handling: Graceful error handling during service initialization
Installation
pnpm add @geekmidas/servicesQuick Start
Define a Service
import type { Service } from '@geekmidas/services';
import type { EnvironmentParser } from '@geekmidas/envkit';
import { Kysely } from 'kysely';
// Define your database service
const databaseService = {
serviceName: 'database' as const,
async register(envParser: EnvironmentParser<{}>) {
const config = envParser.create((get) => ({
url: get('DATABASE_URL').string(),
ssl: get('DATABASE_SSL').string().transform(Boolean).default('false')
})).parse();
const db = new Kysely({ /* config */ });
await db.connection().execute('SELECT 1'); // Health check
return db;
}
} satisfies Service<'database', Kysely<Database>>;
export { databaseService };Use Services in Constructs
import { e } from '@geekmidas/constructs/endpoints';
import { databaseService } from './services/database';
import { z } from 'zod';
export const getUser = e
.get('/users/:id')
.params(z.object({ id: z.string() }))
.services([databaseService])
.handle(async ({ params, services }) => {
// services.database is fully typed as Kysely<Database>
const user = await services.database
.selectFrom('users')
.where('id', '=', params.id)
.selectAll()
.executeTakeFirstOrThrow();
return user;
});Service Discovery
The ServiceDiscovery class manages service lifecycle and dependency injection:
import { ServiceDiscovery } from '@geekmidas/services';
import { ConsoleLogger } from '@geekmidas/logger/console';
import { EnvironmentParser } from '@geekmidas/envkit';
const logger = new ConsoleLogger();
const envParser = new EnvironmentParser(process.env).create(() => ({})).parse();
const serviceDiscovery = ServiceDiscovery.getInstance(logger, envParser);
// Services are lazily initialized
const database = await serviceDiscovery.discover(databaseService);Creating Services
Database Service
import type { Service } from '@geekmidas/services';
import { Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';
const databaseService = {
serviceName: 'database' as const,
async register(envParser: EnvironmentParser<{}>) {
const config = envParser.create((get) => ({
host: get('DB_HOST').string(),
port: get('DB_PORT').string().transform(Number).default('5432'),
database: get('DB_NAME').string(),
user: get('DB_USER').string(),
password: get('DB_PASSWORD').string(),
ssl: get('DB_SSL').string().transform(Boolean).default('false')
})).parse();
const db = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({
host: config.host,
port: config.port,
database: config.database,
user: config.user,
password: config.password,
ssl: config.ssl
})
})
});
return db;
}
} satisfies Service<'database', Kysely<Database>>;Redis/Cache Service
import type { Service } from '@geekmidas/services';
import { UpstashCache } from '@geekmidas/cache/upstash';
const cacheService = {
serviceName: 'cache' as const,
async register(envParser: EnvironmentParser<{}>) {
const config = envParser.create((get) => ({
url: get('UPSTASH_REDIS_URL').string().url(),
token: get('UPSTASH_REDIS_TOKEN').string()
})).parse();
return new UpstashCache({
url: config.url,
token: config.token
});
}
} satisfies Service<'cache', UpstashCache<any>>;Email Service
import type { Service } from '@geekmidas/services';
import { createEmailClient } from '@geekmidas/emailkit';
import * as templates from './email-templates';
const emailService = {
serviceName: 'email' as const,
async register(envParser: EnvironmentParser<{}>) {
const config = envParser.create((get) => ({
host: get('SMTP_HOST').string(),
port: get('SMTP_PORT').string().transform(Number),
user: get('SMTP_USER').string(),
pass: get('SMTP_PASS').string(),
from: get('EMAIL_FROM').string().email()
})).parse();
return createEmailClient({
smtp: {
host: config.host,
port: config.port,
auth: {
user: config.user,
pass: config.pass
}
},
templates,
defaults: { from: config.from }
});
}
} satisfies Service<'email', ReturnType<typeof createEmailClient>>;Event Publisher Service
import type { Service } from '@geekmidas/services';
import type { EventPublisher, PublishableMessage } from '@geekmidas/events';
type UserEvents =
| PublishableMessage<'user.created', { userId: string }>
| PublishableMessage<'user.updated', { userId: string }>;
const userEventPublisher = {
serviceName: 'userEventPublisher' as const,
async register(envParser: EnvironmentParser<{}>) {
const config = envParser.create((get) => ({
publisherUrl: get('EVENT_PUBLISHER_URL').string()
})).parse();
const { Publisher } = await import('@geekmidas/events');
return Publisher.fromConnectionString<UserEvents>(config.publisherUrl);
}
} satisfies Service<'userEventPublisher', EventPublisher<UserEvents>>;Multiple Services
Inject multiple services into a construct:
import { e } from '@geekmidas/constructs/endpoints';
import { databaseService } from './services/database';
import { cacheService } from './services/cache';
import { emailService } from './services/email';
import { z } from 'zod';
export const createUser = e
.post('/users')
.body(z.object({
name: z.string(),
email: z.string().email()
}))
.services([databaseService, cacheService, emailService])
.handle(async ({ body, services }) => {
// Check cache first
const cached = await services.cache.get(`user:${body.email}`);
if (cached) {
return cached;
}
// Create user in database
const user = await services.database
.insertInto('users')
.values(body)
.returningAll()
.executeTakeFirstOrThrow();
// Send welcome email
await services.email.sendTemplate('welcome', {
to: user.email,
props: { name: user.name }
});
// Cache result
await services.cache.set(`user:${user.email}`, user, 3600);
return user;
});Service Lifecycle
Lazy Initialization
Services are only initialized when first requested:
const serviceDiscovery = ServiceDiscovery.getInstance(logger, envParser);
// Not initialized yet
const service1 = databaseService;
// Initialized here
const db = await serviceDiscovery.discover(databaseService);
// Reuses same instance (singleton)
const db2 = await serviceDiscovery.discover(databaseService);
assert(db === db2); // trueSingleton Pattern
ServiceDiscovery ensures each service is a singleton:
// First call initializes the service
const db1 = await serviceDiscovery.discover(databaseService);
// Subsequent calls return cached instance
const db2 = await serviceDiscovery.discover(databaseService);
const db3 = await serviceDiscovery.discover(databaseService);
// All references point to same instance
console.log(db1 === db2 && db2 === db3); // trueError Handling
Handle service initialization errors gracefully:
const databaseService = {
serviceName: 'database' as const,
async register(envParser: EnvironmentParser<{}>) {
try {
const config = envParser.create((get) => ({
url: get('DATABASE_URL').string()
})).parse();
const db = new Kysely({ /* config */ });
// Test connection
await db.connection().execute('SELECT 1');
return db;
} catch (error) {
throw new Error(`Failed to initialize database: ${error.message}`);
}
}
} satisfies Service<'database', Kysely<Database>>;Testing
Mock services in tests:
import { ServiceDiscovery } from '@geekmidas/services';
import { vi } from 'vitest';
// Create mock database
const mockDb = {
selectFrom: vi.fn(() => ({
where: vi.fn(() => ({
selectAll: vi.fn(() => ({
executeTakeFirstOrThrow: vi.fn().mockResolvedValue({
id: '1',
name: 'Test User'
})
}))
}))
}))
};
// Create mock service
const mockDatabaseService = {
serviceName: 'database' as const,
async register() {
return mockDb;
}
} satisfies Service<'database', typeof mockDb>;
// Use in tests
const serviceDiscovery = ServiceDiscovery.getInstance(logger, envParser);
const db = await serviceDiscovery.discover(mockDatabaseService);
// Test your codeService Patterns
Repository Pattern
import type { Service } from '@geekmidas/services';
class UserRepository {
constructor(private db: Kysely<Database>) {}
async findById(id: string) {
return this.db
.selectFrom('users')
.where('id', '=', id)
.selectAll()
.executeTakeFirst();
}
async create(data: NewUser) {
return this.db
.insertInto('users')
.values(data)
.returningAll()
.executeTakeFirstOrThrow();
}
}
const userRepositoryService = {
serviceName: 'userRepository' as const,
async register(envParser: EnvironmentParser<{}>) {
const db = await databaseService.register(envParser);
return new UserRepository(db);
}
} satisfies Service<'userRepository', UserRepository>;Service Composition
import type { Service } from '@geekmidas/services';
class UserService {
constructor(
private repository: UserRepository,
private email: EmailClient,
private cache: Cache
) {}
async createUser(data: NewUser) {
const user = await this.repository.create(data);
await this.email.sendTemplate('welcome', {
to: user.email,
props: { name: user.name }
});
await this.cache.set(`user:${user.id}`, user, 3600);
return user;
}
}
const userServiceService = {
serviceName: 'userService' as const,
async register(envParser: EnvironmentParser<{}>) {
const repository = await userRepositoryService.register(envParser);
const email = await emailService.register(envParser);
const cache = await cacheService.register(envParser);
return new UserService(repository, email, cache);
}
} satisfies Service<'userService', UserService>;TypeScript Types
import type { Service } from '@geekmidas/services';
// Service interface
interface Service<TName extends string = string, TInstance = unknown> {
serviceName: TName;
register(envParser: EnvironmentParser<{}>): Promise<TInstance> | TInstance;
}
// Infer service name
type ServiceName<T> = T extends Service<infer N, any> ? N : never;
// Infer service instance type
type ServiceInstance<T> = T extends Service<any, infer I> ? I : never;Best Practices
1. Use satisfies for Type Safety
// ✅ Use satisfies to ensure correct implementation
const service = {
serviceName: 'myService' as const,
async register(envParser) {
return new MyService();
}
} satisfies Service<'myService', MyService>;
// ❌ Don't use type annotation (loses type inference)
const service: Service = {
serviceName: 'myService',
async register(envParser) {
return new MyService();
}
};2. Use as const for Service Names
// ✅ Literal type for better type inference
serviceName: 'database' as const
// ❌ String type loses specificity
serviceName: 'database'3. Validate Configuration
async register(envParser: EnvironmentParser<{}>) {
const config = envParser.create((get) => ({
url: get('DATABASE_URL').string().url(), // Validates URL format
port: get('PORT').string().transform(Number).default('5432'),
ssl: get('SSL').string().transform(Boolean).default('false')
})).parse();
return new Database(config);
}4. Test Connections
async register(envParser: EnvironmentParser<{}>) {
const db = new Database(config);
// Test connection during initialization
await db.connection().execute('SELECT 1');
return db;
}Related Packages
- @geekmidas/constructs - Uses services for dependency injection
- @geekmidas/envkit - Environment configuration for services
- @geekmidas/logger - Logging within services
- @geekmidas/cache - Cache services
- @geekmidas/events - Event publisher services
License
MIT
