Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Kysely com TypeScript e SQL

Atualizado em: 20 de agosto de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

Kysely com TypeScript e SQL permite escrever consultas com ajuda do compilador sem abandonar o modelo relacional. A biblioteca funciona como um query builder tipado: tabelas e colunas são descritas em interfaces, e as operações geram SQL parametrizado para PostgreSQL, MySQL, SQLite e outros dialetos.

O Kysely não tenta transformar todas as tabelas em objetos com ciclo de vida próprio. Ele mantém joins, agregações, subqueries e transações próximos do SQL. Isso oferece controle, mas exige conhecimento de índices, locks, planos de execução e modelagem. Para revisar os fundamentos, consulte PostgreSQL e o que é TypeScript.

Quando usar Kysely?

Ele é indicado para equipes que desejam consultas explícitas, autocomplete e verificação de nomes e tipos. Também funciona bem em projetos que já possuem um banco modelado e não querem adotar um ORM com unidade de trabalho, lazy loading ou entidades mutáveis.

A tipagem descreve a visão do código sobre o banco. Ela não inspeciona automaticamente cada mudança feita fora do projeto. Migrations e geração de tipos precisam fazer parte do fluxo de desenvolvimento para evitar divergência.

Instalação

npm init -y
npm install kysely pg
npm install --save-dev @types/pg typescript

Use um lockfile e uma versão suportada do Node.js. A URL de conexão deve vir de variável validada ou gerenciador de segredos.

Modelando os tipos

import type {
  ColumnType,
  Generated,
  Insertable,
  Selectable,
  Updateable
} from 'kysely';

interface UserTable {
  id: Generated<string>;
  email: string;
  name: string;
  created_at: ColumnType<Date, Date | string | undefined, never>;
}

interface OrderTable {
  id: Generated<string>;
  user_id: string;
  total_cents: number;
  status: 'pending' | 'paid' | 'cancelled';
  created_at: ColumnType<Date, Date | string | undefined, never>;
}

interface Database {
  users: UserTable;
  orders: OrderTable;
}

type User = Selectable<UserTable>;
type NewUser = Insertable<UserTable>;
type UserUpdate = Updateable<UserTable>;

Generated indica que a coluna pode ser omitida na inserção. ColumnType permite diferenciar o tipo selecionado, inserido e atualizado. Essas definições não substituem constraints reais no PostgreSQL.

Criando a conexão

import { Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  connectionTimeoutMillis: 3000,
  idleTimeoutMillis: 30000,
  statement_timeout: 5000,
  application_name: 'orders-api'
});

export const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool })
});

O pool é compartilhado pela instância do Kysely. Não crie uma nova instância por requisição. Em aplicações com múltiplos processos, considere a soma de conexões. Veja pool PostgreSQL no Node.js.

Inserindo registros

const user = await db
  .insertInto('users')
  .values({
    email: input.email.toLowerCase(),
    name: input.name
  })
  .returning(['id', 'email', 'name', 'created_at'])
  .executeTakeFirstOrThrow();

Selecione explicitamente o retorno para não expor colunas futuras por acidente. Mapeie violações de unique para um erro de conflito e não envie mensagens internas do PostgreSQL ao cliente.

Consultas e filtros

const orders = await db
  .selectFrom('orders')
  .select(['id', 'total_cents', 'status', 'created_at'])
  .where('user_id', '=', userId)
  .where(eb =>
    cursor
      ? eb('created_at', '<', cursor)
      : eb.val(true)
  )
  .orderBy('created_at', 'desc')
  .orderBy('id', 'desc')
  .limit(50)
  .execute();

Use uma ordenação estável para paginação por cursor. Em tabelas grandes, cursor costuma escalar melhor que OFFSET. O índice deve acompanhar filtro e ordenação, por exemplo (user_id, created_at desc, id desc).

Joins tipados

const order = await db
  .selectFrom('orders as o')
  .innerJoin('users as u', 'u.id', 'o.user_id')
  .select([
    'o.id as orderId',
    'o.total_cents as totalCents',
    'o.status',
    'u.name as customerName',
    'u.email as customerEmail'
  ])
  .where('o.id', '=', orderId)
  .executeTakeFirst();

Aliases mantêm o resultado claro. O compilador ajuda a detectar colunas inexistentes e tipos incompatíveis, mas a autorização continua sendo responsabilidade da aplicação. Confirme que o usuário pode acessar o recurso antes de devolver os dados.

Transações

await db.transaction().execute(async trx => {
  const order = await trx
    .insertInto('orders')
    .values({
      user_id: userId,
      total_cents: totalCents,
      status: 'pending'
    })
    .returning('id')
    .executeTakeFirstOrThrow();

  await trx
    .insertInto('outbox_events')
    .values({
      aggregate_id: order.id,
      event_type: 'order.created',
      payload: JSON.stringify({
        orderId: order.id,
        totalCents
      })
    })
    .execute();
});

Mantenha transações curtas. Não faça requisições HTTP ou espere processamento externo enquanto segura conexões e locks. O outbox registra o evento na mesma transação e permite publicá-lo depois.

Atualização concorrente

const result = await db
  .updateTable('orders')
  .set({ status: 'paid' })
  .where('id', '=', orderId)
  .where('status', '=', 'pending')
  .returning('id')
  .executeTakeFirst();

if (!result) {
  throw new ConflictError('Order cannot be paid');
}

Incluir o estado esperado no WHERE evita sobrescrever uma transição concorrente. Em outros casos, adicione uma coluna version e incremente-a de forma atômica.

SQL avançado

O Kysely fornece o template sql para expressões específicas. Valores interpolados são parametrizados quando usados corretamente.

import { sql } from 'kysely';

const stats = await db
  .selectFrom('orders')
  .select([
    'status',
    sql<number>`count(*)::int`.as('total')
  ])
  .where('created_at', '>=', startDate)
  .groupBy('status')
  .execute();

Não use sql.raw com entrada do usuário. Identificadores dinâmicos, ordenação e nomes de tabela devem passar por listas permitidas.

Migrations

O Kysely possui primitivas para migrations, mas o SQL gerado deve ser revisado. Mantenha cada migration pequena e ordenada, execute sob lock e registre quais foram aplicadas.

await db.schema
  .createIndex('orders_user_created_idx')
  .on('orders')
  .columns(['user_id', 'created_at'])
  .execute();

Alterações grandes em produção podem bloquear tabelas. Teste com volume representativo e use etapas compatíveis. Adicione colunas antes de obrigá-las, preencha dados e só depois imponha NOT NULL.

Validação em runtime

Os tipos TypeScript desaparecem em execução. Valide body, query e parâmetros antes de construir a consulta. O guia de Zod no TypeScript mostra schemas e safeParse.

Mapeie somente campos permitidos. Não passe o objeto recebido diretamente para update, pois isso permite mass assignment e futuras colunas podem ser alteradas sem intenção.

Plugins e logs

Plugins podem transformar queries e observar execução. Use logs de duração e operação, mas remova valores sensíveis. Não registre a URL completa de conexão nem parâmetros pessoais.

Slow queries devem ser analisadas com EXPLAIN ANALYZE. Tipagem não cria índices e não corrige planos ruins.

Segurança

  • use credenciais com privilégio mínimo;
  • ative TLS conforme a rede;
  • parametrize valores;
  • restrinja identificadores dinâmicos;
  • valide entrada em runtime;
  • defina timeouts de conexão e statement;
  • revise migrations;
  • teste backup e restauração.

Testes de integração

Suba um PostgreSQL isolado, aplique migrations e teste queries reais. Mocks não reproduzem constraints, tipos, transações ou locks. Use rollback por teste ou bancos separados para manter isolamento.

import test from 'node:test';
import assert from 'node:assert/strict';

test('impede transição duplicada', async () => {
  const order = await createPendingOrder();
  await payOrder(order.id);

  await assert.rejects(
    () => payOrder(order.id),
    error => error.code === 'ORDER_STATE_CONFLICT'
  );
});

Para regras sem banco, use testes unitários. Consulte testes unitários com Jest.

Observabilidade

Meça duração, erros por código, aquisição de conexão, pool ativo e timeouts. Agrupe queries por operação estável, não pelo SQL completo com valores. Relacione métricas de banco à latência e erros HTTP.

Erros comuns

  • Tratar tipos como validação: dados externos continuam desconhecidos;
  • Criar db por requisição: pools e conexões se multiplicam;
  • Selecionar todas as colunas: contratos e privacidade ficam frágeis;
  • Concatenar ordenação: identificadores dinâmicos podem gerar injeção;
  • Ignorar plano de execução: uma consulta válida pode ser lenta;
  • Migrations destrutivas de uma vez: deploys antigos deixam de funcionar.

Checklist

  • interfaces refletem o schema atual;
  • constraints existem no banco;
  • pool e timeouts estão configurados;
  • queries selecionam campos explícitos;
  • entrada é validada;
  • transações são curtas;
  • migrations foram revisadas;
  • consultas críticas possuem testes e índices.

Referências oficiais

Conclusão

Kysely com TypeScript e SQL oferece uma camada leve para construir consultas tipadas sem esconder a estrutura relacional. A API melhora autocomplete, refatoração e clareza de resultados.

A qualidade em produção continua dependendo de constraints, validação, índices, transações e migrations seguras. Com esses cuidados, o Kysely funciona como uma ponte eficiente entre o compilador TypeScript e o comportamento real do PostgreSQL.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita