@lerianstudio/sindarian-server
v1.1.0
Published
Sindarian Server
Downloads
637
Readme
Sindarian Server
A lightweight, NestJS-inspired framework designed specifically for Next.js applications. Build scalable APIs with familiar decorator-based architecture while leveraging Next.js's serverless capabilities.
( Features
- =� NestJS-like API - Familiar decorators and patterns
- � Next.js Optimized - Built for serverless environments
- =� Dependency Injection - Powered by Inversify
- =� Decorator-based Routing - Clean, declarative route definitions
- =' Middleware Support - Guards, interceptors, pipes, and exception filters
- ✅ Zod Validation - Built-in schema validation with Zod
- =� TypeScript First - Full type safety out of the box
- <� Lightweight - Minimal overhead for fast cold starts
=� Quick Start
Installation
npm install @lerianstudio/sindarian-server reflect-metadata inversifyBasic Setup
- Create your first controller:
// controllers/user.controller.ts
import { Controller, Get, Post, Param, Body, BaseController } from '@lerianstudio/sindarian-server'
@Controller('/users')
export class UserController extends BaseController {
@Get()
async findAll() {
return { users: [] }
}
@Get(':id')
async findOne(@Param('id') id: string) {
return { id, name: 'John Doe' }
}
@Post()
async create(@Body() userData: any) {
return { id: 1, ...userData }
}
}- Create a service:
// services/user.service.ts
import { injectable } from 'inversify'
@injectable()
export class UserService {
async findAll() {
return [{ id: 1, name: 'John' }]
}
}- Set up your module:
// app.module.ts
import { Module } from '@lerianstudio/sindarian-server'
import { UserController } from './controllers/user.controller'
import { UserService } from './services/user.service'
@Module({
controllers: [UserController],
providers: [UserService]
})
export class AppModule {}- Create the application:
// app.ts
import 'reflect-metadata'
import { ServerFactory } from '@lerianstudio/sindarian-server'
import { AppModule } from './app.module'
export const app = ServerFactory.create(AppModule)
// Optional: Configure global settings
app.setGlobalPrefix('/api/v1')- Set up Next.js API routes:
// pages/api/[...slug].ts (Pages Router)
// or app/api/[...slug]/route.ts (App Router)
import { app } from './app'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest, context: any) {
return app.handler(request, context)
}
export async function POST(request: NextRequest, context: any) {
return app.handler(request, context)
}
export async function PUT(request: NextRequest, context: any) {
return app.handler(request, context)
}
export async function DELETE(request: NextRequest, context: any) {
return app.handler(request, context)
}
export async function PATCH(request: NextRequest, context: any) {
return app.handler(request, context)
}=� Core Concepts
Controllers
Controllers handle incoming requests and return responses:
@Controller('/posts')
export class PostController extends BaseController {
constructor(
@inject(PostService) private postService: PostService
) {
super()
}
@Get()
async findAll(@Query() query: any) {
return this.postService.findAll(query)
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.postService.findById(id)
}
@Post()
async create(@Body() createPostDto: any) {
return this.postService.create(createPostDto)
}
@Put(':id')
async update(@Param('id') id: string, @Body() updatePostDto: any) {
return this.postService.update(id, updatePostDto)
}
@Delete(':id')
async remove(@Param('id') id: string) {
return this.postService.remove(id)
}
}Dependency Injection
Use Inversify's powerful DI system:
@injectable()
export class PostService {
constructor(
@inject(DatabaseService) private db: DatabaseService,
@inject(LoggerService) private logger: LoggerService
) {}
async findAll() {
this.logger.log('Fetching all posts')
return this.db.posts.findMany()
}
}Modules
Organize your application with modules:
@Module({
imports: [DatabaseModule], // Import other modules
controllers: [PostController],
providers: [
PostService,
{
provide: 'CONFIG',
useValue: { apiKey: process.env.API_KEY }
},
{
provide: CacheService,
useFactory: () => new CacheService({ ttl: 3600 })
}
]
})
export class PostModule {}Exception Handling
Create custom exception filters:
@Catch(ValidationError)
export class ValidationFilter implements ExceptionFilter {
async catch(exception: ValidationError, host: ArgumentsHost) {
return NextResponse.json(
{
message: 'Validation failed',
errors: exception.errors
},
{ status: 400 }
)
}
}
// Apply globally via app
app.useGlobalFilters(new ValidationFilter())
// Or register via module providers (supports multiple filters)
@Module({
providers: [
{
provide: APP_FILTER,
useClass: ValidationFilter
},
{
provide: APP_FILTER,
useClass: DatabaseErrorFilter
}
]
})
export class AppModule {}
// Or on specific controllers
@Controller('/users')
@UseFilters(ValidationFilter)
export class UserController extends BaseController {}Note: When registering multiple filters via APP_FILTER, they execute in reverse order (last registered runs first), allowing more specific filters to handle exceptions before general ones.
Interceptors
Add cross-cutting concerns with interceptors:
export class LoggingInterceptor implements Interceptor {
async intercept(context: ExecutionContext, next: CallHandler) {
const start = Date.now()
const result = await next.handle()
const duration = Date.now() - start
console.log(`${context.getClass().name}.${context.getHandler().name} - ${duration}ms`)
return result
}
}
// Apply globally via app
app.useGlobalInterceptors(new LoggingInterceptor())
// Or register via module providers (supports multiple interceptors)
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor
},
{
provide: APP_INTERCEPTOR,
useClass: TransformInterceptor
}
]
})
export class AppModule {}
// Or on specific controllers
@Controller('/users')
@UseInterceptors(LoggingInterceptor)
export class UserController extends BaseController {}Note: Multiple APP_INTERCEPTOR and APP_PIPE providers are supported and execute in reverse registration order (last registered runs first).
Guards
Guards determine whether a request should be handled by the route handler. They're commonly used for authentication and authorization:
import { CanActivate, ExecutionContext, UseGuards } from '@lerianstudio/sindarian-server'
import { injectable } from 'inversify'
@injectable()
export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest()
const token = request.headers.get('authorization')
if (!token) {
return false // Will throw ForbiddenApiException
}
// Validate token...
return true
}
}
// Apply to controller
@Controller('/admin')
@UseGuards(AuthGuard)
export class AdminController extends BaseController {
@Get()
async dashboard() {
return { message: 'Welcome to admin' }
}
}
// Or apply to specific methods
@Controller('/users')
export class UserController extends BaseController {
@Get()
async findAll() {
return { users: [] } // Public route
}
@Delete(':id')
@UseGuards(AuthGuard)
async remove(@Param('id') id: string) {
return { deleted: id } // Protected route
}
}Register global guards via module providers:
@Module({
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard
}
]
})
export class AppModule {}
// Or globally via app
app.useGlobalGuards(new AuthGuard())Note: Guards execute before interceptors and pipes. If a guard returns false, a ForbiddenApiException is thrown.
Pipes
Transform and validate input data with pipes:
import { PipeTransform, ArgumentMetadata, UsePipes } from '@lerianstudio/sindarian-server'
@injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10)
if (isNaN(val)) {
throw new Error('Validation failed: not a number')
}
return val
}
}
// Apply to controller or method
@Controller('/items')
@UsePipes(ParseIntPipe)
export class ItemController extends BaseController {
@Get(':id')
async findOne(@Param('id') id: number) {
return this.itemService.findById(id)
}
}Register global pipes via module providers:
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe
}
]
})
export class AppModule {}Validation with Zod
Built-in Zod integration for schema validation:
import { createZodDto, ZodValidationPipe, UsePipes } from '@lerianstudio/sindarian-server'
import { z } from 'zod'
// Define your schema
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18).optional()
})
// Create a DTO class from the schema
const CreateUserDto = createZodDto(CreateUserSchema)
// Use in your controller with the validation pipe
@Controller('/users')
@UsePipes(ZodValidationPipe)
export class UserController extends BaseController {
@Post()
async create(@Body() userData: typeof CreateUserDto) {
// userData is validated and typed
return this.userService.create(userData)
}
}The ZodValidationPipe automatically validates incoming data against Zod schemas and throws a ValidationApiException on validation errors.
Logger Service
Use the built-in logger for consistent application logging:
import { ConsoleLogger, LoggerService } from '@lerianstudio/sindarian-server'
// Use the default ConsoleLogger
const logger = new ConsoleLogger({ prefix: 'MyApp' })
logger.log('Application started')
logger.error('Something went wrong')
logger.warn('This is a warning')
logger.debug('Debug information')
// Or inject as a service
@injectable()
export class MyService {
constructor(private logger: ConsoleLogger) {}
doSomething() {
this.logger.log('Doing something...')
}
}Available log levels: verbose, debug, log, warn, error, fatal
HttpService
Abstract base class for building API client services with built-in error handling:
import { HttpService, FetchModuleOptions } from '@lerianstudio/sindarian-server'
import { injectable } from 'inversify'
@injectable()
export class MyApiService extends HttpService {
protected async createDefaults(): Promise<FetchModuleOptions> {
return {
baseUrl: 'https://api.example.com',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getToken()}`
}
}
}
async getUsers() {
return this.get<User[]>('/users')
}
async createUser(data: CreateUserDto) {
return this.post<User>('/users', {
body: JSON.stringify(data)
})
}
async uploadFile(data: { file: File; name: string }) {
return this.postFormData<UploadResponse>('/upload', data)
}
}The HttpService provides:
- HTTP methods:
get,post,put,patch,delete,head - FormData support:
postFormData,patchFormData - Automatic error handling with typed exceptions
- Lifecycle hooks:
onBeforeFetch,onAfterFetch,catch
<� Parameter Decorators
Extract data from requests with decorators:
@Controller('/users')
export class UserController extends BaseController {
@Get(':id/posts')
async getUserPosts(
@Param('id') userId: string,
@Query('page') page: number,
@Query('limit') limit: number,
@Request() req: NextRequest
) {
return this.userService.getPosts(userId, { page, limit })
}
@Post()
async createUser(
@Body() userData: CreateUserDto,
@Request() req: NextRequest
) {
return this.userService.create(userData)
}
}=' Advanced Configuration
Custom Providers
@Module({
providers: [
// Class provider
UserService,
// Value provider
{
provide: 'DATABASE_URL',
useValue: process.env.DATABASE_URL
},
// Factory provider
{
provide: DatabaseService,
useFactory: (context: ResolutionContext) => {
const url = context.container.get('DATABASE_URL')
return new DatabaseService(url)
}
},
// Class-to-class provider
{
provide: 'IUserRepository',
useClass: PostgresUserRepository
}
]
})
export class AppModule {}Request Injection
Access the current request anywhere in your application:
@injectable()
export class AuthService {
constructor(@inject(REQUEST) private request: NextRequest) {}
getCurrentUser() {
const token = this.request.headers.get('authorization')
// Decode token and return user
}
}=� Documentation
- Technical Guide - Deep dive into the framework's architecture and implementation details
- API Reference - Complete API documentation
- Examples - Working examples and use cases
> Contributing
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
=� License
This project is licensed under the ISC License - see the LICENSE file for details.
=O Acknowledgments
- Inspired by NestJS - A progressive Node.js framework
- Built on Inversify - A powerful IoC container for TypeScript
- Designed for Next.js - The React framework for production
=� What's Next?
- [x] Validation pipes with Zod integration
- [x] Guards system for authentication/authorization
- [ ] WebSocket support
- [ ] GraphQL integration
- [ ] CLI tools for scaffolding
- [ ] More built-in interceptors and filters
Sindarian Server - Building the future of Next.js APIs, one decorator at a time. =�
