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 🙏

© 2025 – Pkg Stats / Ryan Hefner

sv-inject

v0.1.12

Published

A lightweight, TypeScript-based dependency injection system designed for Astro applications with SSR support

Readme

sv-inject

A lightweight, TypeScript-based dependency injection system designed primarily for Astro applications, with first-class SSR (Server-Side Rendering) support.

sv-inject provides a framework-agnostic, minimalistic DI system built on modern TypeScript features such as decorators and metadata. It supports request-scoped service containers to ensure safe and isolated service instances in concurrent server environments.


📑 Table of Contents


🚀 Features

  • ✅ Decorator-based service registration (@Injectable()) and dependency injection (svInject())
  • 📦 Request-scoped containers for safe SSR execution
  • ⚙️ Lifecycle hooks (postConstruct)
  • 🔌 Integrations for Astro, Next.js, and other Vite-based SSR frameworks
  • 🛠 Framework-agnostic: use in any modern TypeScript SSR app
  • 📦 No dependencies (besides vite based build tools)
  • 🪶 Minimalistic: 2kb gzipped 6.1 kb minified

Inspiration

This library is heavily inspired by Angulars DI system. It is designed to be framework-agnostic, so that it can be used in any modern SSR app. But originally conceptualized for AstroJS + Svelte to have a lightweight DI system, shared accross components and Islands.

important: since this framework should be minimal, no automatic resolution of "module/component scope" will be implemented. If you fear "global state pollution", you have to add containers to the root injection context, yourself.


📦 Installation

npm install sv-inject

🔰 Getting Started

⚙️ TypeScript Configuration

This library uses experimental decorators and metadata reflection. You must enable these options in your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Creating Injectable Classes

For Informative purposes, multiple ways of defining Injectables are shown here. All of them are equivalent, but with different names to have an "Archetype."

@Store() // for stores
@Injectable() // non specific
@Service() // services
@Controller() // controllers
@Util() // utilities
import { Service, inject } from 'sv-inject';

// Define a service
@Service()
class UserService {
  constructor() {
    // Service initialization
  }

  getUser(id: string) {
    // Implementation
  }

  // Optional lifecycle hook
  postConstruct() {
    // Initialization after construction
  }
}

// Define a service with dependencies
@Service()
class AuthService {
  private userService = svInject(UserService);
  
  constructor() {
    // Service initialization
  }

  authenticate(credentials: any) {
    // Use injected userService
    const user = this.userService.getUser(credentials.userId);
    // Implementation
  }
}

Or as explicit constructor injection:

import { Service } from 'sv-inject';

// Define a service with dependencies
@Service()
class AuthService {

    constructor(
        private userService = svInject(UserService)
    ) {
// Service initialization
    }

    authenticate(credentials: any) {
// Use injected userService
        const user = this.userService.getUser(credentials.userId);
// Implementation
    }
}

Using Services in Components

import { svInject } from 'sv-inject';
import { AuthService } from './services/auth.service';

// In a component
const authService = svInject(AuthService);
authService.authenticate(credentials);

The svInject() function pulls the service instance from the current injection context (global or request-scoped).

🌐 SSR Support

In SSR (Server-Side Rendering), every HTTP request runs in a shared server environment. Without proper isolation, service instances can leak data between users. sv-inject solves this by providing request-scoped containers — each request gets its own dependency graph, preventing cross-request pollution.

These request containers are created with makeInjectionContext(), wrapping any async render or middleware logic.

For SSR capabilities in Astro, the application must be built in SSR mode (servermode), at least in standalone mode. The makeInjectionContext function is designed for Astro but works with any other Vite-based SSR app that has an app render cycle within a promise-based rendering system.

Astro Example

import { type ApplicationConfig } from 'sv-inject';
import { makeInjectionContext } from "sv-inject/server"



// In an Astro middleware
export const MyMiddleware = defineMiddleware(async (context, next) => {
  return new Promise<Response>(async (resolve, reject) => {
    // Define request-specific configuration
    const ssrConfig: ApplicationConfig = [
      {
        token: REQUEST_TOKEN,
        provide: context.request,
      },
      {
        token: COOKIES_TOKEN,
        provide: context.cookies,
      }
    ];

    // Create a unique container for this request
    makeInjectionContext(async () => {
      const response = await next();
      resolve(response);
    }, ssrConfig).catch(reject);
  });
});

SSR Detection

By default this library uses import.meta.env.SSR to detect SSR contexts. If this is not available in your framework you have to use something like this before first container initialisation:

import { setSSRDetection } from 'sv-inject';

setSSRDetection(() => typeof window === "undefined");

NextJS Example

import { type ApplicationConfig, setSSRDetection } from 'sv-inject';
import { makeInjectionContext } from "sv-inject/server";
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

setSSRDetection(() => typeof window === "undefined");

// In a NextJS middleware
export async function middleware(request: NextRequest) {
  // Define request-specific configuration
  const ssrConfig: ApplicationConfig = [
    {
      token: { id: 'REQUEST' },
      provide: request,
    }
  ];

  // Create a unique container for this request and wrap the response
  return makeInjectionContext(async () => {
    // Process the request and get the response
    // You can perform any operations that need the injection context here
    const response = NextResponse.next();

    // You can also modify the response if needed
    response.headers.set('x-middleware-cache', 'no-cache');

    return response;
  }, ssrConfig);
}

📚 API Reference

Core Decorators

@Service()

Marks a class as injectable, making it available for dependency injection.

@Service()
class UserService {
  // Class implementation
}

Injection Methods

svInject<T>(token: new (...args: any[]) => T): T

Injects a service by class constructor, throws error if its not registered.

const userService = svInject(UserService);

svInjectOptional<T>(token: Tokenizable<T>): T | undefined

Injects a service by token, returning undefined if not found.

const request = svInjectOptional<Request>(REQUEST_TOKEN);

Container Management

initContainer(): Container

Initializes or retrieves the DI container if it is already initialized. A container will be initialized per request or on CSR on first render.

const container = initContainer();

Configuration Methods

setGlobalAppConfig(config: ApplicationConfig)

Sets the global application configuration for the application. In SSR mode, this is used to set the global configuration for the server, but it will not be request-scoped.

Setting the global config must happen in a suitable place of the app, before any container is initialized. Suitable places should be: index.ts or similar.

import { createToken } from "./sv-inject";

const API_URL_TOKEN = createToken<string>("API_URL")

setGlobalAppConfig([
  {
    token: API_URL_TOKEN,
    provide: 'https://api.example.com'
  }
]);

Providers and Factories

sv-inject supports registering values and factories via the Provider type and the container API.

Provider type shape:

export type Provider<T = any> = {
  token: Tokenizable<T>;
  provide?: T;         // directly provide an instance/value (singleton within the container)
  factory?: () => T;   // provide a factory that is executed on every injection
}

You can use providers in your ApplicationConfig (global or request-scoped via makeInjectionContext):

import { createToken, type ApplicationConfig } from 'sv-inject';

// Example abstract contract, this abstract class is now used as a key for the injection context
@Service()
export abstract class Logger {
  abstract log(msg: string): void;
}

// Concrete implementation
class ConsoleLogger extends Logger {
  log(msg: string) { console.log(msg); }
}

// Concrete implementation 2
class RemoteLogger extends Logger {
    log(msg: string) { RemoteLogService.sendLog(msg); }
}


// Configure using a factory so that a specific implementation is injected for an abstract key
const config: ApplicationConfig = [
  {
    token: Logger,          // abstract token or abstract class can be used as the key
    factory: () => {
        if(isDevMode()){
            return new ConsoleLogger();
        }
        return new RemoteLogger();
    },
  },
];

Registering factories programmatically with a token:

import { createToken, initContainer } from 'sv-inject';
const RemoteLogger = createToken<Logger>("LOGGER");
// At startup or within SSR request configuration
const container = initContainer();
container.registerFactory(LOGGER_TOKEN, () => new ConsoleLogger());

Important notes about factories:

  • Factories are re-executed on every injection or container.get(...) call for that token/key.
  • If you need a singleton-like behavior, either:
    • Use provide with a pre-built instance, or
    • Memoize inside your factory:
    • memo/cache clean up should be taken with care as this is a source of memory leaks
const memoLoggerFactory = (() => {
  let cached: Logger | undefined;
  return () => {
      if(isDevMode()){
          return chached ??= new ConsoleLogger();
      }
      return cached ??= new RemoteLogger();
  };
})();

initContainer().registerFactory(LOGGER_TOKEN, memoLoggerFactory);

SSR Utilities

makeInjectionContext<T>(callback: () => Promise<T>, config?: ApplicationConfig): Promise<T>

Creates an injection context utilizing async local storage and an application container. This method is mandatory for SSR request context aware injection containers.

await makeInjectionContext(async () => {
  // Your SSR code here
  return response;
}, config);

🔑 Token-Based Injection

sv-inject supports token-based dependency injection, allowing you to inject services, values, or request-specific objects (like cookies or headers) without relying solely on class constructors.

Defining Tokens

A token can be created with a unique id:

import { createToken } from "sv-inject";
const REQUEST_TOKEN = createToken("REQUEST");

🟡 Important: All tokens must be unique and non-empty. Failing to do so may result in collisions or unexpected injection behavior.

✅ Application-Scoped Token Usage

It is strongly advised to define all tokens within your ApplicationConfig:

import { createToken, setGlobalAppConfig } from "sv-inject";

const REQUEST_TOKEN = createToken("REQUEST");

const config: ApplicationConfig = [
  {
    token: REQUEST_TOKEN,
    provide: request
  },
];

setGlobalAppConfig(config)

For the global AppConfig (or initial tokens/services etc):

  • For Global and CSR with setGlobalAppConfig(config) in index of your application.

    ⚠️ ON SSR: Important everything in this config will NOT be request scoped All configs that are request scoped on SSR have to be considered "optional" in client-side code.

  • For SSR with makeInjectionContext(asyncfun, ssrConfig).

  • The GlobalAppConfig and the passed SSR Config are merged together and applied in order.

These Providers are bound to the request lifecycle, ensuring safe and isolated access per user.

🧩 Optional (e.g. exclusive SSR) Tokens

Tokens that only exist during SSR should be marked as optional using:

import { createToken, svInjectOptional } from "sv-inject";;

const SSR_ONLY_TOKEN = createToken<MyType>("SSR_ONLY_TOKEN");

const value = svInjectOptional(SSR_ONLY_TOKEN);

This avoids runtime errors when rendering in non-SSR or static contexts.

⚙️ Advanced Usage

Debugging

If you need to debug your DI, you can enable debug logging by:

import { SvDebugLogger } from "sv-inject";

SvDebugLogger.default.enable();

⚠️ Lifecycle Awareness

In a case where you need to perform a Injection exactly in the same time as a Provider is created, e.g. At "AppConfiguration" level, or somewhere Post app Initialization, you can use the postConstruct lifecycle hook, to inject the service after the provider is created, Or use svInject on demand at method/function level, to avoid null/undefined injections.

This should be avoided in most cases, as it makes testing and debugging more difficult.

@Service()
class Example {
  private request: Request;

  postConstruct() {
    this.request = svInject({ id: 'REQUEST' });
  }
}

🔄 Singleton Scope and Manual Registration

All services in sv-inject are singletons by default within their container context.

If a service or state needs to be recreated on each injection: Use a factory. If as instance needs to be created later than app initialization and injected dynamically, register it manually using:

initContainer().registerProvider(token, instance);

For example:

import { createToken } from "sv-inject";

const LOCALE_TOKEN = createToken<string>("LOCALE");

initContainer().registerProvider(LOCALE_TOKEN, 'en-US');

🔄 Injection Order

  • Injection order is resolved automatically by the DI container.
  • This ensures predictable and safe injection flow, even across complex service graphs.

Circular dependency

  • Circular dependencies are not resolved automatically.
  • If you need to inject a circular dependency:
  • don't
  • delegate the injection either into postConstruct or the method that needs it.
  • or use a factory with custom constructor logic

No warranty is given for circular dependency resolution.

❓ Why Request-Scoped Containers Matter

  • In SSR environments, services may hold user-specific or request-specific state.
  • Without isolation, shared service instances can leak data between users — a serious security issue.
  • sv-inject creates a unique container per request, so services are safely scoped and reset for each incoming call.
  • You can also inject request-specific tokens (like REQUEST, COOKIES, SESSION, etc.) into your services.