TanStack Start é um framework full-stack baseado em TanStack Router. Ele suporta SSR de documento completo, streaming, funções de servidor, bundling e muito mais, alimentado por TanStack Router e Vite.
Criar uma nova aplicação TanStack Start
Use a CLI interativa para criar uma nova aplicação TanStack Start.
bun create @tanstack/start@latest my-tanstack-appIniciar o servidor de desenvolvimento
Mude para o diretório do projeto e execute o servidor de desenvolvimento com Bun.
cd my-tanstack-app
bun --bun run devIsso inicia o servidor de desenvolvimento Vite com Bun.
Atualizar scripts no package.json
Modifique o campo scripts no seu package.json prefixando os comandos Vite CLI com bun --bun. Isso garante que o Bun execute a CLI do Vite para tarefas comuns como dev, build e preview.
{
"scripts": {
"dev": "bun --bun vite dev",
"build": "bun --bun vite build",
"serve": "bun --bun vite preview"
}
}Hospedagem
Para hospedar sua aplicação TanStack Start, você pode usar Nitro ou um servidor Bun personalizado para implantações de produção.
Nitro
Adicionar Nitro ao seu projeto
Adicione Nitro ao seu projeto. Esta ferramenta permite implantar sua aplicação TanStack Start em diferentes plataformas.
bun add nitroAtualizar seu arquivo vite.config.ts
Atualize seu arquivo vite.config.ts para incluir os plugins necessários para TanStack Start com Bun.
// other imports...
import { nitro } from "nitro/vite";
const config = defineConfig({
plugins: [
tanstackStart(),
nitro({ preset: "bun" }),
// other plugins...
],
});
export default config;NOTE
O preset `bun` é opcional, mas ele configura a saída de build especificamente para o runtime do Bun.Atualizar o comando de início
Certifique-se de que os scripts build e start estão presentes no seu arquivo package.json:
{
"scripts": {
"build": "bun --bun vite build",
// Os arquivos .output são criados pelo Nitro quando você executa `bun run build`.
// Não é necessário ao implantar na Vercel.
"start": "bun run .output/server/index.mjs"
}
}NOTE
Você **não** precisa do script `start` personalizado ao implantar na Vercel.Implantar sua aplicação
Confira um de nossos guias para implantar sua aplicação em um provedor de hospedagem.
NOTE
Ao implantar na Vercel, você pode adicionar `"bunVersion": "1.x"` ao seu arquivo `vercel.json`, ou adicioná-lo à configuração `nitro` no seu arquivo `vite.config.ts`:Aviso
Não use o preset bun do Nitro ao implantar na Vercel.
export default defineConfig({
plugins: [
tanstackStart(),
nitro({
preset: "bun",
vercel: {
functions: {
runtime: "bun1.x",
},
},
}),
],
});Servidor Personalizado
NOTE
Esta implementação de servidor personalizado é baseada no [template Bun do TanStack](https://github.com/TanStack/router/blob/main/examples/react/start-bun/server.ts). Ela fornece controle granular sobre o serviço de ativos estáticos, incluindo gerenciamento de memória configurável que pré-carrega arquivos pequenos na memória para serviço rápido enquanto serve arquivos maiores sob demanda. Esta abordagem é útil quando você precisa de controle preciso sobre o uso de recursos e comportamento de carregamento de ativos em implantações de produção.Criar o servidor de produção
Crie um arquivo server.ts na raiz do seu projeto com a seguinte implementação de servidor personalizado:
/**
* Servidor de Produção TanStack Start com Bun
*
* Um servidor de produção de alta performance para aplicações TanStack Start que
* implementa carregamento inteligente de ativos estáticos com gerenciamento de memória configurável.
*
* Recursos:
* - Estratégia de carregamento híbrido (pré-carregar arquivos pequenos, servir arquivos grandes sob demanda)
* - Filtragem de arquivos configurável com padrões de inclusão/exclusão
* - Geração de resposta eficiente em memória
* - Headers de cache prontos para produção
*
* Variáveis de Ambiente:
*
* PORT (number)
* - Número da porta do servidor
* - Padrão: 3000
*
* ASSET_PRELOAD_MAX_SIZE (number)
* - Tamanho máximo do arquivo em bytes para pré-carregar na memória
* - Arquivos maiores que este serão servidos sob demanda do disco
* - Padrão: 5242880 (5MB)
* - Exemplo: ASSET_PRELOAD_MAX_SIZE=5242880 (5MB)
*
* ASSET_PRELOAD_INCLUDE_PATTERNS (string)
* - Lista separada por vírgulas de padrões glob para arquivos para incluir
* - Se especificado, apenas arquivos correspondentes são elegíveis para pré-carregamento
* - Padrões são correspondidos apenas com nomes de arquivo, não caminhos completos
* - Exemplo: ASSET_PRELOAD_INCLUDE_PATTERNS="*.js,*.css,*.woff2"
*
* ASSET_PRELOAD_EXCLUDE_PATTERNS (string)
* - Lista separada por vírgulas de padrões glob para arquivos para excluir
* - Aplicado após padrões de inclusão
* - Padrões são correspondidos apenas com nomes de arquivo, não caminhos completos
* - Exemplo: ASSET_PRELOAD_EXCLUDE_PATTERNS="*.map,*.txt"
*
* ASSET_PRELOAD_VERBOSE_LOGGING (boolean)
* - Habilitar logging detalhado de arquivos carregados e ignorados
* - Padrão: false
* - Defina como "true" para habilitar saída verbosa
*
* ASSET_PRELOAD_ENABLE_ETAG (boolean)
* - Habilitar geração de ETag para ativos pré-carregados
* - Padrão: true
* - Defina como "false" para desabilitar suporte a ETag
*
* ASSET_PRELOAD_ENABLE_GZIP (boolean)
* - Habilitar compressão Gzip para ativos elegíveis
* - Padrão: true
* - Defina como "false" para desabilitar compressão Gzip
*
* ASSET_PRELOAD_GZIP_MIN_SIZE (number)
* - Tamanho mínimo do arquivo em bytes necessário para compressão Gzip
* - Arquivos menores que este não serão comprimidos
* - Padrão: 1024 (1KB)
*
* ASSET_PRELOAD_GZIP_MIME_TYPES (string)
* - Lista separada por vírgulas de tipos MIME elegíveis para compressão Gzip
* - Suporta correspondência parcial para tipos terminando com "/"
* - Padrão: text/,application/javascript,application/json,application/xml,image/svg+xml
*
* Uso:
* bun run server.ts
*/
import path from 'node:path'
// Configuração
const SERVER_PORT = Number(process.env.PORT ?? 3000)
const CLIENT_DIRECTORY = './dist/client'
const SERVER_ENTRY_POINT = './dist/server/server.js'
// Utilitários de logging para saída profissional
const log = {
info: (message: string) => {
console.log(`[INFO] ${message}`)
},
success: (message: string) => {
console.log(`[SUCCESS] ${message}`)
},
warning: (message: string) => {
console.log(`[WARNING] ${message}`)
},
error: (message: string) => {
console.log(`[ERROR] ${message}`)
},
header: (message: string) => {
console.log(`\n${message}\n`)
},
}
// Pré-carregamento configurado a partir de variáveis de ambiente
const MAX_PRELOAD_BYTES = Number(
process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024, // 5MB padrão
)
// Analisar padrões de inclusão separados por vírgula (sem padrões)
const INCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((pattern: string) => convertGlobToRegExp(pattern))
// Analisar padrões de exclusão separados por vírgula (sem padrões)
const EXCLUDE_PATTERNS = (process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((pattern: string) => convertGlobToRegExp(pattern))
// Flag de logging verboso
const VERBOSE = process.env.ASSET_PRELOAD_VERBOSE_LOGGING === 'true'
// Feature opcional de ETag
const ENABLE_ETAG = (process.env.ASSET_PRELOAD_ENABLE_ETAG ?? 'true') === 'true'
// Feature opcional de Gzip
const ENABLE_GZIP = (process.env.ASSET_PRELOAD_ENABLE_GZIP ?? 'true') === 'true'
const GZIP_MIN_BYTES = Number(process.env.ASSET_PRELOAD_GZIP_MIN_SIZE ?? 1024) // 1KB
const GZIP_TYPES = (
process.env.ASSET_PRELOAD_GZIP_MIME_TYPES ??
'text/,application/javascript,application/json,application/xml,image/svg+xml'
)
.split(',')
.map((v) => v.trim())
.filter(Boolean)
/**
* Converte um padrão glob simples em uma expressão regular
* Suporta * wildcard para corresponder quaisquer caracteres
*/
function convertGlobToRegExp(globPattern: string): RegExp {
// Escapar caracteres especiais regex exceto *, depois substituir * por .*
const escapedPattern = globPattern
.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&')
.replace(/\*/g, '.*')
return new RegExp(`^${escapedPattern}$`, 'i')
}
/**
* Computar ETag para um dado buffer de dados
*/
function computeEtag(data: Uint8Array): string {
const hash = Bun.hash(data)
return `W/"${hash.toString(16)}-${data.byteLength.toString()}"`
}
/**
* Metadados para ativos estáticos pré-carregados
*/
interface AssetMetadata {
route: string
size: number
type: string
}
/**
* Ativo em memória com suporte a ETag e Gzip
*/
interface InMemoryAsset {
raw: Uint8Array
gz?: Uint8Array
etag?: string
type: string
immutable: boolean
size: number
}
/**
* Resultado do processo de pré-carregamento de ativos estáticos
*/
interface PreloadResult {
routes: Record<string, (req: Request) => Response | Promise<Response>>
loaded: AssetMetadata[]
skipped: AssetMetadata[]
}
/**
* Verificar se um arquivo é elegível para pré-carregamento baseado nos padrões configurados
*/
function isFileEligibleForPreloading(relativePath: string): boolean {
const fileName = relativePath.split(/[/\\]/).pop() ?? relativePath
// Se padrões de inclusão forem especificados, o arquivo deve corresponder a pelo menos um
if (INCLUDE_PATTERNS.length > 0) {
if (!INCLUDE_PATTERNS.some((pattern) => pattern.test(fileName))) {
return false
}
}
// Se padrões de exclusão forem especificados, o arquivo não deve corresponder a nenhum
if (EXCLUDE_PATTERNS.some((pattern) => pattern.test(fileName))) {
return false
}
return true
}
/**
* Verificar se um tipo MIME é compressível
*/
function isMimeTypeCompressible(mimeType: string): boolean {
return GZIP_TYPES.some((type) =>
type.endsWith('/') ? mimeType.startsWith(type) : mimeType === type,
)
}
/**
* Comprimir dados condicionalmente baseado no tamanho e tipo MIME
*/
function compressDataIfAppropriate(
data: Uint8Array,
mimeType: string,
): Uint8Array | undefined {
if (!ENABLE_GZIP) return undefined
if (data.byteLength < GZIP_MIN_BYTES) return undefined
if (!isMimeTypeCompressible(mimeType)) return undefined
try {
return Bun.gzipSync(data.buffer as ArrayBuffer)
} catch {
return undefined
}
}
/**
* Criar função handler de resposta com suporte a ETag e Gzip
*/
function createResponseHandler(
asset: InMemoryAsset,
): (req: Request) => Response {
return (req: Request) => {
const headers: Record<string, string> = {
'Content-Type': asset.type,
'Cache-Control': asset.immutable
? 'public, max-age=31536000, immutable'
: 'public, max-age=3600',
}
if (ENABLE_ETAG && asset.etag) {
const ifNone = req.headers.get('if-none-match')
if (ifNone && ifNone === asset.etag) {
return new Response(null, {
status: 304,
headers: { ETag: asset.etag },
})
}
headers.ETag = asset.etag
}
if (
ENABLE_GZIP &&
asset.gz &&
req.headers.get('accept-encoding')?.includes('gzip')
) {
headers['Content-Encoding'] = 'gzip'
headers['Content-Length'] = String(asset.gz.byteLength)
const gzCopy = new Uint8Array(asset.gz)
return new Response(gzCopy, { status: 200, headers })
}
headers['Content-Length'] = String(asset.raw.byteLength)
const rawCopy = new Uint8Array(asset.raw)
return new Response(rawCopy, { status: 200, headers })
}
}
/**
* Criar padrão glob composto a partir de padrões de inclusão
*/
function createCompositeGlobPattern(): Bun.Glob {
const raw = (process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
if (raw.length === 0) return new Bun.Glob('**/*')
if (raw.length === 1) return new Bun.Glob(raw[0])
return new Bun.Glob(`{${raw.join(',')}}`)
}
/**
* Inicializar rotas estáticas com estratégia inteligente de pré-carregamento
* Arquivos pequenos são carregados em memória, arquivos grandes são servidos sob demanda
*/
async function initializeStaticRoutes(
clientDirectory: string,
): Promise<PreloadResult> {
const routes: Record<string, (req: Request) => Response | Promise<Response>> =
{}
const loaded: AssetMetadata[] = []
const skipped: AssetMetadata[] = []
log.info(`Carregando ativos estáticos de ${clientDirectory}...`)
if (VERBOSE) {
console.log(
`Tamanho máximo de pré-carregamento: ${(MAX_PRELOAD_BYTES / 1024 / 1024).toFixed(2)} MB`,
)
if (INCLUDE_PATTERNS.length > 0) {
console.log(
`Padrões de inclusão: ${process.env.ASSET_PRELOAD_INCLUDE_PATTERNS ?? ''}`,
)
}
if (EXCLUDE_PATTERNS.length > 0) {
console.log(
`Padrões de exclusão: ${process.env.ASSET_PRELOAD_EXCLUDE_PATTERNS ?? ''}`,
)
}
}
let totalPreloadedBytes = 0
try {
const glob = createCompositeGlobPattern()
for await (const relativePath of glob.scan({ cwd: clientDirectory })) {
const filepath = path.join(clientDirectory, relativePath)
const route = `/${relativePath.split(path.sep).join(path.posix.sep)}`
try {
// Obter metadados do arquivo
const file = Bun.file(filepath)
// Ignorar se o arquivo não existir ou estiver vazio
if (!(await file.exists()) || file.size === 0) {
continue
}
const metadata: AssetMetadata = {
route,
size: file.size,
type: file.type || 'application/octet-stream',
}
// Determinar se o arquivo deve ser pré-carregado
const matchesPattern = isFileEligibleForPreloading(relativePath)
const withinSizeLimit = file.size <= MAX_PRELOAD_BYTES
if (matchesPattern && withinSizeLimit) {
// Pré-carregar arquivos pequenos em memória com suporte a ETag e Gzip
const bytes = new Uint8Array(await file.arrayBuffer())
const gz = compressDataIfAppropriate(bytes, metadata.type)
const etag = ENABLE_ETAG ? computeEtag(bytes) : undefined
const asset: InMemoryAsset = {
raw: bytes,
gz,
etag,
type: metadata.type,
immutable: true,
size: bytes.byteLength,
}
routes[route] = createResponseHandler(asset)
loaded.push({ ...metadata, size: bytes.byteLength })
totalPreloadedBytes += bytes.byteLength
} else {
// Servir arquivos grandes ou filtrados sob demanda
routes[route] = () => {
const fileOnDemand = Bun.file(filepath)
return new Response(fileOnDemand, {
headers: {
'Content-Type': metadata.type,
'Cache-Control': 'public, max-age=3600',
},
})
}
skipped.push(metadata)
}
} catch (error: unknown) {
if (error instanceof Error && error.name !== 'EISDIR') {
log.error(`Falha ao carregar ${filepath}: ${error.message}`)
}
}
}
// Mostrar visão geral detalhada dos arquivos apenas quando o modo verboso estiver habilitado
if (VERBOSE && (loaded.length > 0 || skipped.length > 0)) {
const allFiles = [...loaded, ...skipped].sort((a, b) =>
a.route.localeCompare(b.route),
)
// Calcular comprimento máximo do caminho para alinhamento
const maxPathLength = Math.min(
Math.max(...allFiles.map((f) => f.route.length)),
60,
)
// Formatar tamanho do arquivo com KB e tamanho gzip real
const formatFileSize = (bytes: number, gzBytes?: number) => {
const kb = bytes / 1024
const sizeStr = kb < 100 ? kb.toFixed(2) : kb.toFixed(1)
if (gzBytes !== undefined) {
const gzKb = gzBytes / 1024
const gzStr = gzKb < 100 ? gzKb.toFixed(2) : gzKb.toFixed(1)
return {
size: sizeStr,
gzip: gzStr,
}
}
// Estimativa de gzip aproximada (tipicamente 30-70% de compressão) se não houver dados gzip reais
const gzipKb = kb * 0.35
return {
size: sizeStr,
gzip: gzipKb < 100 ? gzipKb.toFixed(2) : gzipKb.toFixed(1),
}
}
if (loaded.length > 0) {
console.log('\n📁 Pré-carregado em memória:')
console.log(
'Path │ Size │ Gzip Size',
)
loaded
.sort((a, b) => a.route.localeCompare(b.route))
.forEach((file) => {
const { size, gzip } = formatFileSize(file.size)
const paddedPath = file.route.padEnd(maxPathLength)
const sizeStr = `${size.padStart(7)} kB`
const gzipStr = `${gzip.padStart(7)} kB`
console.log(`${paddedPath} │ ${sizeStr} │ ${gzipStr}`)
})
}
if (skipped.length > 0) {
console.log('\n💾 Servido sob demanda:')
console.log(
'Path │ Size │ Gzip Size',
)
skipped
.sort((a, b) => a.route.localeCompare(b.route))
.forEach((file) => {
const { size, gzip } = formatFileSize(file.size)
const paddedPath = file.route.padEnd(maxPathLength)
const sizeStr = `${size.padStart(7)} kB`
const gzipStr = `${gzip.padStart(7)} kB`
console.log(`${paddedPath} │ ${sizeStr} │ ${gzipStr}`)
})
}
}
// Mostrar informações verbosas detalhadas se habilitado
if (VERBOSE) {
if (loaded.length > 0 || skipped.length > 0) {
const allFiles = [...loaded, ...skipped].sort((a, b) =>
a.route.localeCompare(b.route),
)
console.log('\n📊 Informações detalhadas dos arquivos:')
console.log(
'Status │ Path │ MIME Type │ Reason',
)
allFiles.forEach((file) => {
const isPreloaded = loaded.includes(file)
const status = isPreloaded ? 'MEMORY' : 'ON-DEMAND'
const reason =
!isPreloaded && file.size > MAX_PRELOAD_BYTES
? 'muito grande'
: !isPreloaded
? 'filtrado'
: 'pré-carregado'
const route =
file.route.length > 30
? file.route.substring(0, 27) + '...'
: file.route
console.log(
`${status.padEnd(12)} │ ${route.padEnd(30)} │ ${file.type.padEnd(28)} │ ${reason.padEnd(10)}`,
)
})
} else {
console.log('\n📊 Nenhum arquivo encontrado para exibir')
}
}
// Log de resumo após a lista de arquivos
console.log() // Linha vazia para separação
if (loaded.length > 0) {
log.success(
`Pré-carregado ${String(loaded.length)} arquivos (${(totalPreloadedBytes / 1024 / 1024).toFixed(2)} MB) em memória`,
)
} else {
log.info('Nenhum arquivo pré-carregado em memória')
}
if (skipped.length > 0) {
const tooLarge = skipped.filter((f) => f.size > MAX_PRELOAD_BYTES).length
const filtered = skipped.length - tooLarge
log.info(
`${String(skipped.length)} arquivos serão servidos sob demanda (${String(tooLarge)} muito grandes, ${String(filtered)} filtrados)`,
)
}
} catch (error) {
log.error(
`Falha ao carregar arquivos estáticos de ${clientDirectory}: ${String(error)}`,
)
}
return { routes, loaded, skipped }
}
/**
* Inicializar o servidor
*/
async function initializeServer() {
log.header('Iniciando Servidor de Produção')
// Carregar handler do servidor TanStack Start
let handler: { fetch: (request: Request) => Response | Promise<Response> }
try {
const serverModule = (await import(SERVER_ENTRY_POINT)) as {
default: { fetch: (request: Request) => Response | Promise<Response> }
}
handler = serverModule.default
log.success('Handler da aplicação TanStack Start inicializado')
} catch (error) {
log.error(`Falha ao carregar handler do servidor: ${String(error)}`)
process.exit(1)
}
// Construir rotas estáticas com pré-carregamento inteligente
const { routes } = await initializeStaticRoutes(CLIENT_DIRECTORY)
// Criar servidor Bun
const server = Bun.serve({
port: SERVER_PORT,
routes: {
// Servir ativos estáticos (pré-carregados ou sob demanda)
...routes,
// Fallback para handler TanStack Start para todas as outras rotas
'/*': (req: Request) => {
try {
return handler.fetch(req)
} catch (error) {
log.error(`Erro no handler do servidor: ${String(error)}`)
return new Response('Internal Server Error', { status: 500 })
}
},
},
// Handler de erro global
error(error) {
log.error(
`Erro não tratado no servidor: ${error instanceof Error ? error.message : String(error)}`,
)
return new Response('Internal Server Error', { status: 500 })
},
})
log.success(`Servidor ouvindo em http://localhost:${String(server.port)}`)
}
// Inicializar o servidor
initializeServer().catch((error: unknown) => {
log.error(`Falha ao iniciar servidor: ${String(error)}`)
process.exit(1)
})Atualizar scripts do package.json
Adicione um script start para executar o servidor personalizado:
{
"scripts": {
"build": "bun --bun vite build",
"start": "bun run server.ts"
}
}Build e execução
Faça build da sua aplicação e inicie o servidor:
bun run build
bun run startO servidor será iniciado na porta 3000 por padrão (configurável via variável de ambiente PORT).
Templates
→ Consulte a documentação oficial do TanStack Start para mais informações sobre hospedagem.