Skip to content

Commit ca4920d

Browse files
raymondfengclaude
authored andcommitted
feat: add loopback-core AI agent skill for skills.sh
Add a reusable AI agent skill covering LoopBack 4 core patterns including IoC container, dependency injection, extension points, interceptors, life cycle observers, and component-based architecture. Installable via `npx skills add loopbackio/loopback-next`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
1 parent c3d08a6 commit ca4920d

7 files changed

Lines changed: 1413 additions & 0 deletions

File tree

skills/loopback-core/SKILL.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
---
2+
name: loopback-core
3+
description:
4+
Build large-scale, extensible Node.js applications and frameworks using
5+
LoopBack 4 core patterns. Use when building TypeScript/Node.js projects that
6+
need IoC containers, dependency injection, extension point/extension patterns,
7+
interceptors, life cycle observers, or component-based architecture. Triggers
8+
on tasks involving @loopback/core, @loopback/context, Context, Binding,
9+
@inject, @injectable, @extensionPoint, @extensions, LifeCycleObserver,
10+
Interceptor, or Component patterns. Also use when the user asks about
11+
structuring large-scale Node.js projects for extensibility and composability.
12+
---
13+
14+
# Build Large-Scale Node.js Projects with LoopBack 4 Core
15+
16+
LoopBack 4 core provides an IoC container and DI framework in TypeScript
17+
designed for async-first, large-scale Node.js applications. Import from
18+
`@loopback/core` (not `@loopback/context`). This skill covers only
19+
`@loopback/core` — not REST, repositories, or other LoopBack modules.
20+
21+
## Architecture Decision Tree
22+
23+
1. **Need to manage artifacts and their dependencies?** -> Context & Bindings
24+
([context-and-bindings.md](references/context-and-bindings.md))
25+
2. **Need loose coupling between artifact construction and behavior?** ->
26+
Dependency Injection
27+
([dependency-injection.md](references/dependency-injection.md))
28+
3. **Need a pluggable system where others can add capabilities?** -> Extension
29+
Point/Extension ([extension-points.md](references/extension-points.md))
30+
4. **Need cross-cutting concerns (caching, logging, tracing)?** -> Interceptors
31+
([interceptors-and-observers.md](references/interceptors-and-observers.md))
32+
5. **Need to hook into app start/stop?** -> Life Cycle Observers
33+
([interceptors-and-observers.md](references/interceptors-and-observers.md))
34+
6. **Need runtime-configurable artifacts?** -> Configuration
35+
([configuration.md](references/configuration.md))
36+
7. **Need custom decorators, parameterized classes, or advanced patterns?** ->
37+
Advanced Recipes ([advanced-recipes.md](references/advanced-recipes.md))
38+
39+
## Quick Start: Minimal Extensible Application
40+
41+
```ts
42+
import {
43+
Application,
44+
BindingKey,
45+
Component,
46+
Binding,
47+
createBindingFromClass,
48+
extensionPoint,
49+
extensions,
50+
BindingTemplate,
51+
extensionFor,
52+
injectable,
53+
Getter,
54+
config,
55+
} from '@loopback/core';
56+
57+
// 1. Define the extension contract
58+
export interface Greeter {
59+
language: string;
60+
greet(name: string): string;
61+
}
62+
63+
export const GREETER_EXTENSION_POINT_NAME = 'greeters';
64+
65+
export const asGreeter: BindingTemplate = binding => {
66+
extensionFor(GREETER_EXTENSION_POINT_NAME)(binding);
67+
binding.tag({namespace: 'greeters'});
68+
};
69+
70+
// 2. Define the extension point
71+
@extensionPoint(GREETER_EXTENSION_POINT_NAME)
72+
export class GreetingService {
73+
constructor(
74+
@extensions() private getGreeters: Getter<Greeter[]>,
75+
@config() public readonly options?: {color: string},
76+
) {}
77+
async greet(language: string, name: string): Promise<string> {
78+
const greeters = await this.getGreeters();
79+
const greeter = greeters.find(g => g.language === language);
80+
return greeter ? greeter.greet(name) : `Hello, ${name}!`;
81+
}
82+
}
83+
84+
// 3. Implement extensions
85+
@injectable(asGreeter)
86+
export class EnglishGreeter implements Greeter {
87+
language = 'en';
88+
greet(name: string) {
89+
return `Hello, ${name}!`;
90+
}
91+
}
92+
93+
@injectable(asGreeter)
94+
export class ChineseGreeter implements Greeter {
95+
language = 'zh';
96+
greet(name: string) {
97+
return `${name},你好!`;
98+
}
99+
}
100+
101+
// 4. Bundle into a component
102+
export const GREETING_SERVICE = BindingKey.create<GreetingService>(
103+
'services.GreetingService',
104+
);
105+
106+
export class GreetingComponent implements Component {
107+
bindings: Binding[] = [
108+
createBindingFromClass(GreetingService, {key: GREETING_SERVICE}),
109+
createBindingFromClass(EnglishGreeter),
110+
createBindingFromClass(ChineseGreeter),
111+
];
112+
}
113+
114+
// 5. Compose components via nesting
115+
export class CoreComponent implements Component {
116+
// services: auto-registered service/provider classes
117+
services = [SomeUtilityService];
118+
}
119+
120+
export class AppComponent implements Component {
121+
// components: nested components (registered recursively)
122+
components = [CoreComponent, GreetingComponent];
123+
// services: additional services for this layer
124+
services = [AppSpecificService];
125+
}
126+
127+
// 6. Wire up the application
128+
export class MyApp extends Application {
129+
constructor() {
130+
super({shutdown: {signals: ['SIGINT']}});
131+
this.component(AppComponent);
132+
}
133+
async main() {
134+
const svc = await this.get(GREETING_SERVICE);
135+
console.log(await svc.greet('en', 'World'));
136+
}
137+
}
138+
```
139+
140+
## Core Patterns Summary
141+
142+
| Pattern | Key APIs | When to Use |
143+
| ----------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------- |
144+
| Context & Binding | `Context`, `bind()`, `toClass()`, `toDynamicValue()`, `toProvider()` | Managing artifacts and their dependencies |
145+
| DI | `@inject()`, `@inject.getter()`, `@inject.view()` | Decoupling construction from behavior |
146+
| Extension Point | `@extensionPoint()`, `@extensions()`, `@extensions.list()`, `extensionFor()`, `@injectable()` | Pluggable, open-ended feature sets |
147+
| Interceptor | `@injectable(asGlobalInterceptor())`, `Provider<Interceptor>` | Cross-cutting concerns |
148+
| Observer | `@lifeCycleObserver('group')`, `LifeCycleObserver` | Startup/shutdown hooks (group controls order) |
149+
| Configuration | `@config()`, `@config.view()`, `app.configure()` | Runtime-configurable behavior |
150+
| Component | `Component`, `components[]`, `services[]`, `bindings[]` | Composable packaging of artifacts |
151+
152+
## Key Rules
153+
154+
- Always import from `@loopback/core`, not `@loopback/context`
155+
- Use `BindingKey.create<T>()` for strongly-typed keys
156+
- Extension injection: `@extensions()` returns `Getter<T[]>` (lazy, picks up
157+
dynamic additions); `@extensions.list()` returns `T[]` (eager, simpler when
158+
extensions are static at startup)
159+
- Use `@config()` for artifact configuration, `app.configure(key).to(value)` to
160+
set it
161+
- Use `@injectable(bindingTemplate)` to decorate extension classes — can combine
162+
scope and extension:
163+
`@injectable({scope: BindingScope.SINGLETON}, extensionFor(POINT))`
164+
- Use `createBindingFromClass()` to create bindings that respect `@injectable`
165+
metadata
166+
- Compose components hierarchically: `components[]` for nesting, `services[]`
167+
for auto-registration, `bindings[]` for custom bindings
168+
- Use `BindingScope.SINGLETON` for shared stateful services
169+
- Use `CoreBindings.APPLICATION_INSTANCE` to inject the Application itself
170+
- Use `ContextTags.KEY` to tag bindings with a stable key:
171+
`tags: {[ContextTags.KEY]: MY_KEY}`
172+
- Lifecycle observer groups are sorted alphabetically — use numbered prefixes
173+
(e.g., `'03-setup'`, `'10-app'`) to control startup order
174+
175+
## References
176+
177+
- **Context & Bindings**:
178+
[references/context-and-bindings.md](references/context-and-bindings.md)
179+
creating contexts, binding types, scopes, finding bindings, context hierarchy,
180+
views, components
181+
- **Dependency Injection**:
182+
[references/dependency-injection.md](references/dependency-injection.md)
183+
constructor/property/method injection, getters, views, custom decorators,
184+
custom injectors
185+
- **Extension Points**:
186+
[references/extension-points.md](references/extension-points.md) — defining
187+
contracts, extension point classes, implementing/registering extensions,
188+
configuration
189+
- **Interceptors & Observers**:
190+
[references/interceptors-and-observers.md](references/interceptors-and-observers.md)
191+
— global interceptors, interceptor proxies, life cycle observers, dynamic
192+
config via ContextView
193+
- **Configuration**: [references/configuration.md](references/configuration.md)
194+
`@config()`, `@config.view()`, dynamic config, custom resolvers, sync vs
195+
async
196+
- **Advanced Recipes**:
197+
[references/advanced-recipes.md](references/advanced-recipes.md) — custom
198+
decorators, custom injectors, parameterized class factories, application
199+
scaffolding
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Advanced Recipes
2+
3+
## Table of Contents
4+
5+
- [TypeScript Decorator Configuration](#typescript-decorator-configuration)
6+
- [Custom Decorators](#custom-decorators)
7+
- [Custom Injectors](#custom-injectors)
8+
- [Parameterized Class Factories](#parameterized-class-factories)
9+
- [Explicit Context DI in Interceptors](#explicit-context-di-in-interceptors)
10+
- [Sync vs Async Resolution](#sync-vs-async-resolution)
11+
- [Application Scaffolding Pattern](#application-scaffolding-pattern)
12+
13+
## TypeScript Decorator Configuration
14+
15+
LoopBack 4 decorators (`@inject`, `@injectable`, `@lifeCycleObserver`, etc.)
16+
require these tsconfig settings:
17+
18+
```jsonc
19+
{
20+
"compilerOptions": {
21+
"experimentalDecorators": true,
22+
"emitDecoratorMetadata": true,
23+
},
24+
}
25+
```
26+
27+
- **`experimentalDecorators`** — enables `@decorator` syntax (required for all
28+
LoopBack DI)
29+
- **`emitDecoratorMetadata`** — emits type metadata at runtime so LoopBack can
30+
resolve injection targets by type
31+
32+
Without these, decorators either cause compile errors or silently fail at
33+
runtime.
34+
35+
## Custom Decorators
36+
37+
Create a sugar decorator wrapping `@inject`:
38+
39+
```ts
40+
import {BindingKey, inject} from '@loopback/core';
41+
42+
const CURRENT_USER = BindingKey.create<string>('currentUser');
43+
44+
function whoAmI() {
45+
return inject(CURRENT_USER);
46+
}
47+
48+
class Greeter {
49+
constructor(@whoAmI() private userName: string) {}
50+
hello() {
51+
return `Hello, ${this.userName}`;
52+
}
53+
}
54+
```
55+
56+
Create a decorator using `DecoratorFactory`:
57+
58+
See `@loopback/example-context/src/custom-inject-decorator.ts`.
59+
60+
## Custom Injectors
61+
62+
Inject with custom resolve logic:
63+
64+
```ts
65+
export function env(name: string) {
66+
return inject('', {resolve: () => process.env[name]});
67+
}
68+
69+
class MyService {
70+
constructor(@env('DATABASE_URL') private dbUrl: string) {}
71+
}
72+
```
73+
74+
## Parameterized Class Factories
75+
76+
When top-level decorators can't reference variables, use a class factory:
77+
78+
```ts
79+
import {
80+
BindingAddress,
81+
BindingTag,
82+
Constructor,
83+
Context,
84+
createBindingFromClass,
85+
inject,
86+
injectable,
87+
} from '@loopback/core';
88+
89+
interface Greeter {
90+
hello(): string;
91+
}
92+
93+
function createClassWithDecoration(
94+
bindingKeyForName: BindingAddress<string>,
95+
...tags: BindingTag[]
96+
): Constructor<Greeter> {
97+
@injectable({tags})
98+
class GreeterTemplate implements Greeter {
99+
constructor(@inject(bindingKeyForName) private userName: string) {}
100+
hello() {
101+
return `Hello, ${this.userName}`;
102+
}
103+
}
104+
return GreeterTemplate;
105+
}
106+
107+
// Usage:
108+
const ctx = new Context();
109+
ctx.bind('name1').to('John');
110+
const MyGreeter = createClassWithDecoration('name1', {tags: {prefix: '1'}});
111+
ctx.add(createBindingFromClass(MyGreeter, {key: 'greeter1'}));
112+
const greeter = await ctx.get<Greeter>('greeter1');
113+
```
114+
115+
## Explicit Context DI in Interceptors
116+
117+
Use `instantiateClass` to trigger DI within interceptors or any context where
118+
you need to create a class instance with injection:
119+
120+
```ts
121+
import {inject, instantiateClass} from '@loopback/core';
122+
123+
class InjectionHelper {
124+
constructor(@inject('services.Logger') public readonly logger: Logger) {}
125+
}
126+
127+
const interceptor: Interceptor = async (invocationCtx, next) => {
128+
const helper = await instantiateClass(InjectionHelper, invocationCtx);
129+
helper.logger.info('intercepting...');
130+
return next();
131+
};
132+
```
133+
134+
## Sync vs Async Resolution
135+
136+
- `Context.getSync()` — resolves synchronously (throws if any dependency is
137+
async)
138+
- `Context.get()` — resolves asynchronously (returns Promise)
139+
140+
When `ValueOrPromise` is used, the framework auto-detects: returns a plain value
141+
if all deps are sync, otherwise returns a `Promise`.
142+
143+
## Application Scaffolding Pattern
144+
145+
Standalone application with shutdown handling:
146+
147+
```ts
148+
import {Application, ApplicationConfig} from '@loopback/core';
149+
150+
export class MyApplication extends Application {
151+
constructor(config?: ApplicationConfig) {
152+
super({shutdown: {signals: ['SIGINT']}, ...config});
153+
this.component(CoreComponent);
154+
}
155+
156+
async main() {
157+
await this.start();
158+
const service = await this.get(GREETING_SERVICE);
159+
console.log(await service.greet('en', 'World'));
160+
}
161+
}
162+
```
163+
164+
Layered application with conditional components:
165+
166+
```ts
167+
export class BaseApplication extends Application {
168+
constructor(config?: ApplicationConfig) {
169+
super({shutdown: {signals: ['SIGINT']}, ...config});
170+
this.component(BaseComponent);
171+
}
172+
}
173+
174+
export class MessagingApplication extends BaseApplication {
175+
constructor() {
176+
super();
177+
this.component(MessagingComponent);
178+
}
179+
}
180+
181+
// Entry point
182+
export async function main() {
183+
const app = new MessagingApplication();
184+
app.component(PluginComponent); // add dynamically
185+
app.configure(MY_SERVICE_KEY).to({port: 3000}); // configure before start
186+
await app.start();
187+
return app;
188+
}
189+
```

0 commit comments

Comments
 (0)