create-kit-dev
v1.1.21
Published
CLI para criar um projeto Node.js moderno com TypeScript, esbuild e estrutura inicial configurada.
Maintainers
Readme
O que é o Kit Dev?
Kit Dev é uma CLI para iniciar projetos Node.js com TypeScript sem precisar configurar o ambiente do zero.
Ela prepara o projeto, instala as dependências e deixa prontos os comandos de desenvolvimento e produção.
Você pode usar o Kit Dev apenas como gerador e sistema de build. A injeção de dependência é totalmente opcional.
Início rápido
Com npm:
npx create-kit-devTambém funciona com:
pnpm create kit-dev
yarn create kit-devInforme o nome do projeto. Depois:
cd minha-api
npm run devPronto. A aplicação será recompilada e reiniciada automaticamente quando o código mudar.
O que já vem configurado?
- TypeScript em modo
strict; - esbuild para desenvolvimento e produção;
- watch com reinício automático do Node.js;
- checagem de tipos no build;
- bundle minificado;
- sourcemap externo;
- análise simples do bundle;
- npm, pnpm e Yarn;
- DI opcional, sem decorators.
Comandos
Os scripts são adicionados automaticamente ao package.json.
| Comando | Para que serve |
|---|---|
| npm run dev | Executa a aplicação em desenvolvimento, observa alterações e reinicia o Node.js |
| npm run type | Mantém o TypeScript verificando erros em tempo real |
| npm run build | Verifica os tipos e gera o bundle de produção |
| npm start | Executa o bundle já gerado em dist |
| npm run di | Instala a DI opcional no projeto |
Com pnpm use
pnpm dev,pnpm buildetc. Com Yarn useyarn dev,yarn buildetc.
Desenvolvimento
Na maior parte do tempo você só precisa de:
npm run devO esbuild observa o projeto e, após cada rebuild bem-sucedido, reinicia a aplicação.
Se quiser acompanhar erros TypeScript continuamente em outro terminal:
npm run typeO comando type é opcional. O build já executa uma checagem de tipos antes de gerar o bundle.
Build de produção
npm run buildO build executa, nesta ordem:
- checagem TypeScript com
tsc --noEmit; - bundle com esbuild;
- minificação;
- geração do sourcemap;
- resumo simples do bundle.
Arquivos gerados:
dist/bundle.cjs
dist/bundle.cjs.mapO resumo mostra o tamanho do bundle, quantidade de arquivos de entrada e tempo total do build.
Pacotes listados em dependencies e devDependencies ficam externos ao bundle.
Para executar o resultado:
npm startInjeção de dependência opcional
Você não precisa usar DI para usar o Kit Dev.
Para instalar a DI, execute uma única vez:
npm run diO comando cria o container e habilita o transformer. Depois disso, o script di é removido do package.json e a DI passa a funcionar automaticamente em npm run dev e npm run build.
A DI do Kit Dev não usa decorators, reflect-metadata ou bibliotecas externas de injeção de dependência.
Como a DI funciona
Existem três partes principais:
AppConfigregistra as dependências;- o transformer analisa os tipos TypeScript e descobre dependências do construtor quando possível;
ApplicationContextcria e entrega as instâncias em runtime.
O fluxo é:
AppConfig
↓
providers registrados
↓
createApplicationContext()
↓
container.get(...)A configuração normalmente fica em src/di/providers.ts.
Exemplo completo com interface
Contrato:
// src/domain/repositories/user-repository.ts
export interface UserRepository {
save(name: string): Promise<void>;
}Implementação:
// src/infra/repositories/user-repository-memory.ts
import type { UserRepository } from '../../domain/repositories/user-repository.js';
export class UserRepositoryMemory implements UserRepository {
async save(name: string): Promise<void> {
console.log(`Usuário ${name} salvo`);
}
}Caso de uso:
// src/application/use-cases/create-user.ts
import type { UserRepository } from '../../domain/repositories/user-repository.js';
export class CreateUser {
constructor(private readonly repository: UserRepository) {}
execute(name: string): Promise<void> {
return this.repository.save(name);
}
}Registro:
// src/di/providers.ts
import {
AppConfig,
createApplicationContext,
} from '../../kit-dev/di/container.js';
import { CreateUser } from '../application/use-cases/create-user.js';
import type { UserRepository } from '../domain/repositories/user-repository.js';
import { UserRepositoryMemory } from '../infra/repositories/user-repository-memory.js';
const providers = new AppConfig();
providers.useClass<UserRepository>(UserRepositoryMemory);
providers.useClass(CreateUser);
export const container = createApplicationContext(providers);Uso:
// src/main.ts
import { CreateUser } from './application/use-cases/create-user.js';
import { container } from './di/providers.js';
const createUser = container.get(CreateUser);
await createUser.execute('Marcos');O transformer vê que CreateUser recebe UserRepository no construtor e liga automaticamente esse contrato à implementação UserRepositoryMemory.
Interfaces não existem em runtime. Por isso você registra a interface com useClass<Interface>(Implementacao), mas normalmente resolve uma classe concreta com container.get().
Todas as formas de registrar dependências
useClass() — classes
useClass() é a forma mais comum de registro e pode ser usada de várias maneiras.
1. Classe concreta
Quando a própria classe pode ser usada como token:
class EmailService {}
providers.useClass(EmailService);Depois:
const emailService = container.get(EmailService);Se a classe tiver dependências no construtor, o Kit Dev tenta inferi-las automaticamente:
class SendEmail {
constructor(private readonly emailService: EmailService) {}
}
providers.useClass(EmailService);
providers.useClass(SendEmail);Não é necessário informar [EmailService] manualmente nesse caso.
2. Interface ou type alias como contrato
Interfaces e type aliases não existem em JavaScript. O transformer cria um token interno automaticamente:
import type { UserRepository } from '../domain/user-repository.js';
import { UserRepositoryMemory } from '../infra/user-repository-memory.js';
providers.useClass<UserRepository>(UserRepositoryMemory);Agora qualquer classe cujo construtor dependa de UserRepository pode ser resolvida automaticamente:
class CreateUser {
constructor(private readonly repository: UserRepository) {}
}Para o token automático, o contrato deve ser uma interface ou type alias nomeado e não genérico.
3. Classe abstrata como token
Uma classe abstrata existe em runtime e pode ser usada diretamente como token:
abstract class UserRepositoryBase {
abstract save(name: string): Promise<void>;
}
class UserRepositoryDatabase extends UserRepositoryBase {
async save(name: string): Promise<void> {
// banco de dados
}
}
providers.useClass(UserRepositoryBase, UserRepositoryDatabase);Uma classe pode depender dela normalmente:
class CreateUser {
constructor(private readonly repository: UserRepositoryBase) {}
}4. Dependências informadas manualmente
O transformer não consegue inferir tudo. Valores primitivos, tokens manuais, tipos genéricos, parâmetros opcionais e alguns tipos externos devem ser informados explicitamente.
A ordem do array deve seguir a ordem do construtor:
import { createToken } from '../../kit-dev/di/container.js';
const APP_NAME = createToken<string>('APP_NAME');
class ConfigService {
constructor(readonly appName: string) {}
}
providers.useValue(APP_NAME, 'Minha API');
providers.useClass(ConfigService, [APP_NAME]);Também é possível informar dependências manualmente ao registrar uma interface:
providers.useClass<UserRepository>(UserRepositoryDatabase, [DATABASE]);Ou uma classe abstrata:
providers.useClass(UserRepositoryBase, UserRepositoryDatabase, [DATABASE]);createToken<T>() — tokens manuais
Use createToken<T>() quando não existir uma classe que possa representar a dependência em runtime.
É útil principalmente para strings, números, configurações, clientes externos e outras dependências manuais:
import { createToken } from '../../kit-dev/di/container.js';
export const DATABASE_URL = createToken<string>('DATABASE_URL');
export const PORT = createToken<number>('PORT');Registre o valor:
providers.useValue(DATABASE_URL, process.env.DATABASE_URL!);
providers.useValue(PORT, 3000);E use o mesmo token para resolver:
const databaseUrl = container.get(DATABASE_URL);O token é um symbol. Guarde e reutilize a mesma constante; não crie um novo token com a mesma descrição esperando que ele seja o mesmo token.
useValue() — valor já existente
Use quando a instância ou valor já existe e o container não precisa criá-lo:
const APP_NAME = createToken<string>('APP_NAME');
providers.useValue(APP_NAME, 'Kit Dev');Também funciona com objetos e instâncias:
const config = {
port: 3000,
environment: 'development',
};
const CONFIG = createToken<typeof config>('CONFIG');
providers.useValue(CONFIG, config);Uma classe também pode ser usada como token para uma instância pronta:
providers.useValue(Logger, new Logger());useValue() sempre entrega o mesmo valor registrado.
useFactory() — criação personalizada
Use quando a criação da dependência precisa de lógica própria.
A factory recebe o ApplicationContext, então pode resolver outras dependências:
const DATABASE_URL = createToken<string>('DATABASE_URL');
providers.useValue(DATABASE_URL, process.env.DATABASE_URL!);
providers.useFactory(Database, (container) => {
const url = container.get(DATABASE_URL);
return new Database(url);
});Depois:
const database = container.get(Database);useFactory() é útil para clientes de banco, SDKs, adaptadores, objetos que precisam de configuração e criações que não cabem em um construtor inferido automaticamente.
useExisting() — alias
Use quando dois tokens devem apontar para a mesma instância:
const PRIMARY_DATABASE = createToken<Database>('PRIMARY_DATABASE');
providers.useClass(Database);
providers.useExisting(PRIMARY_DATABASE, Database);Agora:
const database = container.get(Database);
const primaryDatabase = container.get(PRIMARY_DATABASE);
console.log(database === primaryDatabase); // trueuseExisting() não cria outra instância. Ele apenas redireciona um token para outro provider.
imports() — separar providers por módulo
Você não precisa manter todos os registros em um único arquivo.
Crie configurações menores:
// src/di/database-providers.ts
import { AppConfig } from '../../kit-dev/di/container.js';
import { Database } from '../infra/database.js';
export const databaseProviders = new AppConfig()
.useClass(Database);Depois importe no composition root:
// src/di/providers.ts
import {
AppConfig,
createApplicationContext,
} from '../../kit-dev/di/container.js';
import { databaseProviders } from './database-providers.js';
const providers = new AppConfig();
providers.imports(databaseProviders);
export const container = createApplicationContext(providers);Também pode importar várias configurações de uma vez:
providers.imports(
databaseProviders,
userProviders,
emailProviders,
);Se duas configurações registrarem o mesmo token, o Kit Dev lança DependencyInjectionError em vez de sobrescrever silenciosamente o provider.
has() — verificar registro no AppConfig
Antes de criar o container:
providers.useClass(Database);
console.log(providers.has(Database)); // trueEsse has() verifica os registros do AppConfig.
Escopos
singleton — padrão
É o escopo padrão. A instância é criada na primeira resolução e reutilizada pelo container:
providers.useClass(Database);Equivale a:
providers.useClass(Database, [], { scope: 'singleton' });transient
Cria uma nova instância em cada resolução:
providers.useClass(RequestContext, [], { scope: 'transient' });Também pode ser usado com factory:
providers.useFactory(
RequestId,
() => new RequestId(crypto.randomUUID()),
{ scope: 'transient' },
);E com registros que usam contrato:
providers.useClass<UserRepository>(
UserRepositoryMemory,
[],
{ scope: 'transient' },
);Instâncias transient não ficam armazenadas no container e, por isso, não são gerenciadas por close().
Encadeando registros
Os métodos de registro retornam o próprio AppConfig, então podem ser encadeados:
const providers = new AppConfig()
.useValue(APP_NAME, 'Kit Dev')
.useClass(Logger)
.useClass(UserService);Criando o container
Crie o ApplicationContext somente depois de registrar e importar todos os providers:
export const container = createApplicationContext(providers);O container recebe uma cópia da configuração naquele momento. Portanto, faça os registros antes de chamar createApplicationContext().
Métodos do container
get()
Resolve uma dependência. Se o token não existir, lança DependencyInjectionError:
const service = container.get(UserService);getOptional()
Retorna a dependência ou undefined se não estiver registrada:
const logger = container.getOptional(LOGGER);has()
Verifica se o token existe no contexto:
if (container.has(UserService)) {
// registrado
}clearInstances()
Limpa as instâncias armazenadas em cache sem remover os providers:
container.clearInstances();Na próxima resolução, singletons de classe/factory serão criados novamente.
clearInstances() não chama dispose() ou close() nas instâncias antigas. É especialmente útil em testes.
close()
Fecha recursos armazenados pelo container e depois limpa o cache:
await container.close();Se uma instância singleton possuir dispose() ou close(), o Kit Dev chama esse método uma vez durante o fechamento.
Exemplo:
class Database {
async close() {
// encerra conexão
}
}
providers.useClass(Database);
const database = container.get(Database);
// ao encerrar a aplicação
await container.close();Quando a inferência automática funciona?
O Kit Dev consegue inferir dependências de construtores quando elas são representadas por tipos nomeados do projeto, como classes, classes abstratas, interfaces e type aliases suportados.
Exemplo:
class UserService {
constructor(
private readonly repository: UserRepository,
private readonly logger: Logger,
) {}
}
providers.useClass<UserRepository>(UserRepositoryMemory);
providers.useClass(Logger);
providers.useClass(UserService);Nesse caso não é necessário informar manualmente [UserRepository, Logger].
Quando o transformer não consegue inferir uma dependência, informe os tokens manualmente:
providers.useClass(ConfigService, [APP_NAME]);Isso é necessário principalmente para valores primitivos, tipos genéricos, parâmetros opcionais, parâmetros rest e tipos que não podem ser convertidos em um token automático do projeto.
Erros da DI
Problemas de configuração usam DependencyInjectionError, por exemplo:
- token não registrado;
- token duplicado;
- dependência circular;
- configuração inválida;
- erro durante a criação de uma dependência.
Erros de análise do transformer aparecem durante npm run dev ou npm run build e apontam o local do registro que não pôde ser transformado.
Regras rápidas
- use
import typepara interfaces e type aliases usados apenas como tipos; - classes concretas podem usar a própria classe como token;
- classes abstratas podem ser tokens em runtime;
- use
createToken<T>()para valores primitivos e tokens manuais; - o escopo padrão é
singleton; - use
transientquando precisar de uma nova instância a cada resolução; - a ordem das dependências manuais deve ser a mesma do construtor;
- registre e importe tudo antes de
createApplicationContext(); - prefira resolver uma classe concreta que inicia o fluxo em vez de tentar resolver uma interface diretamente.
Estrutura do projeto
Logo após criar um projeto:
minha-api/
├── kit-dev/
│ └── build/
│ ├── dev.cjs
│ └── esbuild.config.cjs
├── src/
│ └── main.ts
├── package.json
└── tsconfig.jsonAo instalar a DI, também são adicionados os arquivos de kit-dev/di e src/di/providers.ts.
A pasta kit-dev faz parte da configuração gerada pelo CLI e pode ser versionada junto com o projeto.
Requisitos
- Node.js 22 ou superior;
- npm, pnpm ou Yarn.
Não é necessário instalar o Kit Dev globalmente.
Licença
MIT
