npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 inversify

Basic Setup

  1. 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 }
  }
}
  1. Create a service:
// services/user.service.ts
import { injectable } from 'inversify'

@injectable()
export class UserService {
  async findAll() {
    return [{ id: 1, name: 'John' }]
  }
}
  1. 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 {}
  1. 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')
  1. 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

> 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. =�