You love NestJS. The decorators feel elegant. The structure keeps your codebase clean. TypeScript integration works well.
Then you add a new service.
You import it in one module. Export it in another. Inject it somewhere else. Suddenly, you get this error:
Nest can't resolve dependencies of the UserService (?).
Please make sure that the argument at index [0]
is available in the current context.
Sound familiar?
The Module Problem
NestJS modules solve a real problem: dependency boundaries. They prevent chaos in large codebases.
But they come at a cost.
Every new service requires you to:
- Create the service with
@Injectable() - Add it to a module's
providersarray - Add it to the
exportsarray if other modules need it - Import the module in every place that uses it
Here's what a typical NestJS module looks like:
// user.module.ts
@Module({
imports: [
DatabaseModule,
ConfigModule,
AuthModule,
LoggingModule,
],
controllers: [UserController],
providers: [
UserService,
UserRepository,
UserValidator,
UserMapper,
],
exports: [UserService, UserRepository],
})
export class UserModule {}
This is boilerplate. Pure configuration overhead.
And it grows. Fast.
A medium-sized app has 20+ modules. Each module has 5-10 providers. You spend more time managing arrays than writing business logic.
The worst part? A typo in any array breaks your entire dependency graph. Debugging takes hours.
What If You Didn't Need Modules?
Rikta is a new TypeScript framework built on Fastify. It keeps the parts developers love about NestJS (decorators, DI, structure) and removes the module configuration entirely.
No imports arrays.
No exports arrays.
No providers arrays.
Decorate your class. Everything works.
// user.service.ts
import { Injectable } from '@riktajs/core';
@Injectable()
export class UserService {
getUsers() {
return ['Alice', 'Bob'];
}
}
// user.controller.ts
import { Controller, Get, Autowired } from '@riktajs/core';
import { UserService } from './user.service';
@Controller('/users')
export class UserController {
@Autowired()
private userService!: UserService;
@Get()
getUsers() {
return this.userService.getUsers();
}
}
That's it. No module file. No registration. Rikta scans your code at startup and resolves dependencies automatically.
How Zero-Config Autowiring Works
Rikta uses three mechanisms:
1. Automatic Discovery
At startup, Rikta scans your project for classes decorated with @Controller() or @Injectable(). It builds a dependency graph from constructor parameters and @Autowired() decorators.
2. Global Provider Registry
All providers live in a single registry. Any injectable class is available anywhere in your app. No need to export or import.
3. Dependency Resolution
Rikta reads TypeScript metadata (via reflect-metadata) to understand what each class needs. It creates instances in the correct order and injects them automatically.
The framework detects circular dependencies during initialization and throws a clear error:
Error: Circular dependency detected:
UserService -> AuthService -> UserService
No cryptic stack traces. No runtime surprises.
A Side-by-Side Comparison
Creating a new feature in NestJS:
// 1. Create the service
@Injectable()
export class PaymentService {
constructor(private configService: ConfigService) {}
}
// 2. Create the module
@Module({
imports: [ConfigModule],
providers: [PaymentService],
exports: [PaymentService],
})
export class PaymentModule {}
// 3. Import in app.module.ts
@Module({
imports: [PaymentModule, /* ... */],
})
export class AppModule {}
// 4. Import in any module that needs it
@Module({
imports: [PaymentModule],
providers: [OrderService],
})
export class OrderModule {}
Creating the same feature in Rikta:
@Injectable()
export class PaymentService {
@Autowired()
private configService!: ConfigService;
}
Done.
Performance Benefits
Rikta uses Fastify as its HTTP layer. Fastify handles up to 30,000 requests per second in benchmarks.
Benchmark results from the official repository:
| Metric | Rikta vs NestJS |
|---|---|
| Startup time | 43% faster |
| GET requests | 41% faster |
| POST requests | 25% faster |
| Route parameters | 46% faster |
Rikta adds minimal overhead (2-5%) over vanilla Fastify. You get dependency injection and decorators without sacrificing speed.
Built-in Zod Validation
NestJS uses class-validator decorators for validation. This works, but you define types twice: once for TypeScript, once for validation.
Rikta integrates Zod natively. Define a schema once. Get validation and TypeScript types automatically.
import { Controller, Post, Body, z } from '@riktajs/core';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
age: z.number().optional(),
});
@Controller('/users')
export class UserController {
@Post()
create(@Body(CreateUserSchema) user: z.infer<typeof CreateUserSchema>) {
// 'user' is validated AND typed
// Invalid requests return 400 automatically
return { created: user };
}
}
No duplicate type definitions. No manual error handling for validation failures.
When To Use Rikta
Rikta works best for:
- Startups and MVPs where development speed matters
- Small to medium teams (1-15 developers)
- Microservices where each service stays focused
- Developers who know NestJS and want less boilerplate
Consider NestJS if:
- You have a large team that needs strict module boundaries
- You require the extensive NestJS ecosystem (specific adapters, plugins)
- Your organization mandates explicit dependency documentation
Getting Started
Create a new project in seconds:
npx @riktajs/cli new my-app
cd my-app
npm run dev
Your API runs at http://localhost:3000.
The CLI generates a complete project with:
- TypeScript configuration optimized for Rikta
- Example controller with REST endpoints
- Example service with dependency injection
- Hot reload development server
Resources
- Documentation: rikta.dev
- GitHub: github.com/riktaHQ/rikta.js
- NPM: @riktajs/core
Rikta is MIT licensed and open source.
Have you tried zero-config frameworks? What do you think about the module vs. autowiring tradeoff? Share your experience in the comments.
Top comments (0)