Você quer um Dashboard Tesouro Direto Next.js no ar antes do café acabar? Em cerca de 150 linhas, você monta a tela com a oferta atual, taxas ao vivo e o gráfico de marcação a mercado. Os dados vêm da API da brapi.dev e ficam guardados no cache na borda (cópia temporária da resposta perto do usuário).
O Tesouro Direto é o produto de renda fixa mais popular do Brasil. Mas, para quem programa, os dados públicos são chatos de usar. O arquivo oficial vem em CSV. Os tickers são internos. E a taxa muda de sentido conforme o indexador do título.
Neste tutorial você vai construir um Dashboard Tesouro Direto Next.js
do zero. A base é o Next.js 15 com App Router (jeito novo do Next.js de
organizar páginas, com a pasta app/). A página consome três endpoints
novos da brapi.dev:
GET /api/v2/treasury/list— oferta paginada com filtrosGET /api/v2/treasury/indicators— snapshot atual (até 20 símbolos)GET /api/v2/treasury/indicators/history— série diária de taxas e preços
Você vai usar Server Components (componente que roda só no servidor — vem pronto pro navegador, sem JavaScript do lado do cliente) para buscar tudo. O cache do Next.js guarda a resposta. Só o gráfico é um client component (componente que precisa rodar no navegador do usuário, ex.: gráficos e botões interativos), feito com Recharts. O Tailwind cuida do visual. Todo o código foi testado contra a API real antes da publicação.
O que vamos construir
São três blocos numa só página. Tudo gira em torno de um Server Component principal:
- Tabela paginada da oferta atual — todos os títulos disponíveis hoje
no Tesouro Direto. Você filtra por indexador (Selic, Prefixado, IPCA+)
pelos
searchParams. - Cards de taxas atuais dos três títulos do sandbox: Tesouro Selic 2031, Tesouro Prefixado com Juros Semestrais 2037 e Tesouro IPCA+ com Juros Semestrais 2060. Cada card mostra a taxa indicativa de venda na forma certa para o indexador.
- Gráfico de marcação a mercado do
sellPricedo Tesouro Prefixado 2037. Ele plota a série diária de fevereiro a maio de 2026 com Recharts.
A página é um Server Component só. Ele faz Promise.all das três chamadas
ao mesmo tempo. Depois do primeiro acesso, o cache do Next.js entrega o
HTML em milissegundos.
Acima: a arquitetura do dashboard. A página principal busca os dados no servidor. Só o gráfico vira client component porque o Recharts precisa do navegador.
Pré-requisitos
- Node.js 20+ (qualquer LTS recente serve)
- bun, pnpm ou npm — os exemplos abaixo usam bun
- Conta brapi.dev no plano Pro para acessar a base completa de títulos
- Quer testar sem assinar? Use os três símbolos sandbox citados acima. Eles funcionam sem token.
Plano necessário
A listagem completa e o histórico diário de todos os títulos pedem o plano Pro. Os três sandbox (Selic 2031, Prefixado 2037, IPCA+ 2060) abrem sem autenticação. Dá pra desenvolver o dashboard inteiro só com eles.
Configurando o projeto
Criando o app Next.js
bun create next-app@latest tesouro-dashboard --typescript --tailwind --app
cd tesouro-dashboardPrefere outro gerenciador? Use um destes:
# pnpm
pnpm create next-app@latest tesouro-dashboard --typescript --tailwind --app
# npm
npx create-next-app@latest tesouro-dashboard --typescript --tailwind --appVariáveis de ambiente
Crie .env.local na raiz:
BRAPI_TOKEN=seu_token_pro_aqui
BRAPI_BASE_URL=https://brapi.dev/api/v2/treasuryA variável BRAPI_TOKEN é lida só no servidor. Ela não tem o prefixo
NEXT_PUBLIC_. Então ela nunca vaza para o navegador do usuário.
Estrutura final
tesouro-dashboard/
├── app/
│ ├── layout.tsx
│ ├── loading.tsx
│ └── page.tsx # Server Component principal
├── components/
│ ├── BondsTable.tsx # Tabela paginada
│ ├── RateCard.tsx # Card de taxa atual
│ └── PriceChart.tsx # Client Component com Recharts
├── lib/
│ ├── brapi.ts # Cliente tipado
│ └── rate.ts # Helper de formatação por indexador
└── .env.localCliente da brapi: lib/brapi.ts
Este arquivo guarda todos os fetches e os tipos dos dados. Tudo aqui roda só no servidor, dentro de Server Components. Por isso o token nunca chega no navegador.
// lib/brapi.ts
const BASE = process.env.BRAPI_BASE_URL || 'https://brapi.dev/api/v2/treasury';
const TOKEN = process.env.BRAPI_TOKEN;
export type Indexer = 'selic' | 'prefixado' | 'ipca' | 'igpm';
export type CouponType = 'zero' | 'semestral';
export type RateInfo = {
rateType:
| 'spreadOverSelic'
| 'nominalAnnualRate'
| 'realAnnualRateOverIpca'
| 'realAnnualRateOverIgpm';
rateUnit: string;
description: string;
};
export type TreasuryBond = {
symbol: string;
bondType: string;
indexer: Indexer;
couponType: CouponType;
maturityDate: string; // YYYY-MM-DD
durationDays: number;
baseDate: string;
buyRate: number | null;
sellRate: number | null;
buyPrice: number | null;
sellPrice: number | null;
basePrice: number | null;
rateInfo: RateInfo;
};
export type Pagination = {
page: number;
limit: number;
totalItems: number;
totalPages: number;
hasNextPage: boolean;
};
export type ListResponse = {
results: TreasuryBond[];
pagination: Pagination;
requestedAt: string;
took: number;
};
export type IndicatorsResponse = {
results: TreasuryBond[];
requestedAt: string;
took: number;
};
export type HistoryPoint = {
baseDate: string;
buyRate: number | null;
sellRate: number | null;
buyPrice: number | null;
sellPrice: number | null;
basePrice: number | null;
};
export type HistorySeries = {
symbol: string;
bondType: string;
indexer: Indexer;
couponType: CouponType;
maturityDate: string;
rateInfo: RateInfo;
history: HistoryPoint[];
};
export type HistoryResponse = {
results: HistorySeries[];
requestedAt: string;
took: number;
};
function headers() {
return TOKEN ? { Authorization: `Bearer ${TOKEN}` } : undefined;
}
// Cache alinhado com a TTL típica da brapi (~15 min para snapshot).
// next.revalidate em segundos: o Next.js armazena no Data Cache.
async function brapiFetch<T>(
path: string,
qs: URLSearchParams,
revalidate: number,
): Promise<T> {
const res = await fetch(`${BASE}${path}?${qs}`, {
headers: headers(),
next: { revalidate, tags: ['treasury'] },
});
if (!res.ok) {
throw new Error(`brapi ${path} falhou: ${res.status}`);
}
return (await res.json()) as T;
}
export async function treasuryList(
params: {
page?: number;
limit?: number;
indexer?: Indexer;
couponType?: CouponType;
search?: string;
sortBy?: 'maturityDate' | 'sellRate' | 'durationDays';
sortOrder?: 'asc' | 'desc';
} = {},
): Promise<ListResponse> {
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v !== undefined) qs.set(k, String(v));
}
return brapiFetch<ListResponse>('/list', qs, 900);
}
export async function treasuryIndicators(
symbols: string[],
): Promise<IndicatorsResponse> {
if (symbols.length === 0 || symbols.length > 20) {
throw new Error('symbols deve ter entre 1 e 20 itens');
}
const qs = new URLSearchParams({ symbols: symbols.join(',') });
return brapiFetch<IndicatorsResponse>('/indicators', qs, 900);
}
export async function treasuryHistory(args: {
symbol: string;
startDate: string; // YYYY-MM-DD
endDate: string; // YYYY-MM-DD
}): Promise<HistorySeries | null> {
const qs = new URLSearchParams({
symbols: args.symbol,
startDate: args.startDate,
endDate: args.endDate,
sortOrder: 'asc',
});
const data = await brapiFetch<HistoryResponse>(
'/indicators/history',
qs,
3600,
);
return data.results[0] ?? null;
}Por que `next.revalidate`?
O fetch com next.revalidate liga o Data Cache do Next.js. Esse é o
jeito de revalidar (refazer o pedido depois de N segundos pra ter dado
novo). A brapi já faz cache na borda. Por isso você não precisa de Redis no
seu app, nem da biblioteca de cache no cliente. O Next.js cuida disso
sozinho. Pra invalidar na hora, chame revalidateTag('treasury') dentro de
uma Server Action.
Helper de formatação: lib/rate.ts
Antes de mostrar qualquer taxa, você precisa lembrar de uma coisa. Os campos
buyRate e sellRate querem dizer coisas diferentes em cada indexador.
Este helper junta todas as regras num lugar só:
// lib/rate.ts
import type { RateInfo } from './brapi';
export function formatRate(rateInfo: RateInfo, rate: number | null): string {
if (rate == null) return '—';
switch (rateInfo.rateType) {
case 'spreadOverSelic':
// Tesouro Selic: spread em p.p. sobre a Selic. Quase sempre próximo de zero.
return `Selic + ${rate.toFixed(2)}% a.a.`;
case 'nominalAnnualRate':
// Tesouro Prefixado: taxa nominal anual contratada.
return `${rate.toFixed(2)}% a.a.`;
case 'realAnnualRateOverIpca':
// Tesouro IPCA+: taxa real anual acima da inflação medida pelo IPCA.
return `IPCA + ${rate.toFixed(2)}% a.a.`;
case 'realAnnualRateOverIgpm':
// Tesouro IGP-M (histórico): taxa real anual acima do IGP-M.
return `IGP-M + ${rate.toFixed(2)}% a.a.`;
}
}
export function indexerLabel(indexer: string): string {
switch (indexer) {
case 'selic':
return 'Pós-fixado (Selic)';
case 'prefixado':
return 'Prefixado';
case 'ipca':
return 'IPCA+';
case 'igpm':
return 'IGP-M';
default:
return indexer;
}
}Veja o que dá no terminal com os três símbolos sandbox. Esses números foram buscados ao vivo antes de publicar este tutorial:
tesouro-selic-01032031 -> Selic + 0.09% a.a.
tesouro-prefixado-com-juros-semestrais-01012037 -> 14.48% a.a.
tesouro-ipca-com-juros-semestrais-15082060 -> IPCA + 7.34% a.a.Este é o erro mais comum em dashboards de Tesouro. Quem mostra "0,09% a.a." no Tesouro Selic 2031, sem o "Selic +" na frente, engana o leitor. Parece que o título rende 0,09% ao ano. Na verdade ele rende a Selic cheia (14,5%) mais 0,09 ponto percentual.
Página principal: app/page.tsx
Este é o coração do Dashboard Tesouro Direto Next.js. Tudo é Server
Component. O Promise.all dispara as três requisições ao mesmo tempo.
Olhe o caminho que cada pedido faz:
Acima: o caminho de render do Server Component. As três chamadas saem ao mesmo tempo. O HTML chega pronto pro navegador. Só depois disso o gráfico hidrata (vira interativo).
// app/page.tsx
import {
treasuryList,
treasuryIndicators,
treasuryHistory,
} from '@/lib/brapi';
import { BondsTable } from '@/components/BondsTable';
import { RateCard } from '@/components/RateCard';
import { PriceChart } from '@/components/PriceChart';
export const metadata = {
title: 'Dashboard Tesouro Direto — brapi',
description:
'Oferta atual do Tesouro Direto, taxas indicativas e marcação a mercado em tempo quase real.',
};
const SANDBOX_SYMBOLS = [
'tesouro-selic-01032031',
'tesouro-prefixado-com-juros-semestrais-01012037',
'tesouro-ipca-com-juros-semestrais-15082060',
];
const CHART_SYMBOL = 'tesouro-prefixado-com-juros-semestrais-01012037';
type SearchParams = {
page?: string;
indexer?: 'selic' | 'prefixado' | 'ipca' | 'igpm';
};
export default async function HomePage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const sp = await searchParams;
const page = Number(sp.page ?? '1');
const indexer = sp.indexer;
const [list, indicators, history] = await Promise.all([
treasuryList({
page,
limit: 10,
indexer,
sortBy: 'maturityDate',
sortOrder: 'asc',
}),
treasuryIndicators(SANDBOX_SYMBOLS),
treasuryHistory({
symbol: CHART_SYMBOL,
startDate: '2026-01-01',
endDate: '2026-05-15',
}),
]);
return (
<main className="container mx-auto py-8 space-y-12 px-4">
<header>
<h1 className="text-3xl font-bold mb-2">Dashboard Tesouro Direto</h1>
<p className="text-gray-600 dark:text-gray-400">
Snapshot às {new Date(indicators.requestedAt).toLocaleString('pt-BR')}{' '}
— dados via{' '}
<a href="https://brapi.dev" className="text-blue-600 hover:underline">
brapi.dev
</a>
.
</p>
</header>
<section>
<h2 className="text-xl font-semibold mb-4">Taxas atuais (sandbox)</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{indicators.results.map((bond) => (
<RateCard key={bond.symbol} bond={bond} />
))}
</div>
</section>
<section>
<h2 className="text-xl font-semibold mb-4">
Marcação a mercado — Tesouro Prefixado 2037
</h2>
{history ? (
<PriceChart series={history.history} />
) : (
<p className="text-gray-500">Sem histórico disponível.</p>
)}
</section>
<section>
<h2 className="text-xl font-semibold mb-4">Oferta atual</h2>
<BondsTable
bonds={list.results}
pagination={list.pagination}
currentIndexer={indexer}
/>
</section>
</main>
);
}Card de taxa: components/RateCard.tsx
Este é um Server Component puro. Não tem 'use client'. Ele recebe um
TreasuryBond e usa o helper para escolher o jeito certo de mostrar a taxa.
// components/RateCard.tsx
import type { TreasuryBond } from '@/lib/brapi';
import { formatRate, indexerLabel } from '@/lib/rate';
export function RateCard({ bond }: { bond: TreasuryBond }) {
const sell = formatRate(bond.rateInfo, bond.sellRate);
const maturity = new Date(bond.maturityDate).toLocaleDateString('pt-BR');
return (
<article className="rounded-lg border border-gray-200 dark:border-gray-700 p-5 bg-white dark:bg-gray-900">
<p className="text-xs uppercase tracking-wide text-gray-500">
{indexerLabel(bond.indexer)}
{bond.couponType === 'semestral' ? ' · com cupom semestral' : ''}
</p>
<h3 className="text-lg font-semibold mt-1">{bond.bondType}</h3>
<p className="text-sm text-gray-500 mb-4">Vencimento: {maturity}</p>
<p className="text-3xl font-bold tabular-nums">{sell}</p>
<p className="text-xs text-gray-500 mt-1">
Taxa indicativa de venda · base {bond.baseDate}
</p>
{bond.sellPrice != null && (
<p className="mt-3 text-sm text-gray-600 dark:text-gray-300">
PU de venda:{' '}
<span className="font-medium tabular-nums">
R$ {bond.sellPrice.toFixed(2)}
</span>
</p>
)}
</article>
);
}Tabela paginada com URL state: components/BondsTable.tsx
Guardar o estado na URL é o jeito certo no App Router. O endereço
?page=2&indexer=ipca vira a fonte da verdade. O Server Component
renderiza de novo com os dados certos. Você não precisa de biblioteca
nenhuma. Só do <Link>.
// components/BondsTable.tsx
import Link from 'next/link';
import type { TreasuryBond, Pagination, Indexer } from '@/lib/brapi';
import { formatRate, indexerLabel } from '@/lib/rate';
const INDEXERS: Array<Indexer | undefined> = [
undefined,
'selic',
'prefixado',
'ipca',
];
export function BondsTable({
bonds,
pagination,
currentIndexer,
}: {
bonds: TreasuryBond[];
pagination: Pagination;
currentIndexer?: Indexer;
}) {
const buildHref = (params: {
page?: number;
indexer?: Indexer;
}): string => {
const qs = new URLSearchParams();
if (params.page && params.page > 1) qs.set('page', String(params.page));
if (params.indexer) qs.set('indexer', params.indexer);
const str = qs.toString();
return str ? `/?${str}` : '/';
};
return (
<div>
<nav className="flex gap-2 mb-4">
{INDEXERS.map((idx) => {
const active = idx === currentIndexer;
return (
<Link
key={idx ?? 'all'}
href={buildHref({ indexer: idx, page: 1 })}
className={`px-3 py-1 rounded text-sm border ${
active
? 'bg-blue-600 text-white border-blue-600'
: 'border-gray-300 text-gray-700 dark:text-gray-300'
}`}
>
{idx ? indexerLabel(idx) : 'Todos'}
</Link>
);
})}
</nav>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-100 dark:bg-gray-800 text-left">
<tr>
<th className="px-3 py-2">Título</th>
<th className="px-3 py-2">Vencimento</th>
<th className="px-3 py-2 text-right">Taxa de venda</th>
<th className="px-3 py-2 text-right">PU venda</th>
</tr>
</thead>
<tbody>
{bonds.map((b) => (
<tr
key={b.symbol}
className="border-b border-gray-200 dark:border-gray-700"
>
<td className="px-3 py-2">
<span className="font-medium">{b.bondType}</span>
{b.couponType === 'semestral' && (
<span className="ml-2 text-xs text-gray-500">
cupom semestral
</span>
)}
</td>
<td className="px-3 py-2 tabular-nums">
{new Date(b.maturityDate).toLocaleDateString('pt-BR')}
</td>
<td className="px-3 py-2 text-right tabular-nums">
{formatRate(b.rateInfo, b.sellRate)}
</td>
<td className="px-3 py-2 text-right tabular-nums">
{b.sellPrice != null
? `R$ ${b.sellPrice.toFixed(2)}`
: '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
<footer className="flex items-center justify-between mt-4 text-sm">
<span className="text-gray-500">
Página {pagination.page} de {pagination.totalPages} ·{' '}
{pagination.totalItems} títulos
</span>
<div className="flex gap-2">
{pagination.page > 1 && (
<Link
href={buildHref({
page: pagination.page - 1,
indexer: currentIndexer,
})}
className="px-3 py-1 border rounded"
>
Anterior
</Link>
)}
{pagination.hasNextPage && (
<Link
href={buildHref({
page: pagination.page + 1,
indexer: currentIndexer,
})}
className="px-3 py-1 border rounded"
>
Próxima
</Link>
)}
</div>
</footer>
</div>
);
}Sem `useState`, sem `nuqs`
Tudo que muda na tabela vive em searchParams. Isso vale para o filtro de
indexador e para a paginação. O App Router entende isso sozinho. Quando o
usuário clica num <Link>, o Server Component renderiza de novo. Você
ganha três coisas de graça: histórico do navegador, link que pode ser
compartilhado e SEO.
Gráfico no cliente: components/PriceChart.tsx
O Recharts precisa do DOM. Por isso este arquivo é um client component. Ele recebe a série já pronta, vinda do Server Component, e desenha o gráfico.
Instale o Recharts:
bun add rechartsO componente:
// components/PriceChart.tsx
'use client';
import {
LineChart,
Line,
XAxis,
YAxis,
Tooltip,
CartesianGrid,
ResponsiveContainer,
} from 'recharts';
import type { HistoryPoint } from '@/lib/brapi';
export function PriceChart({ series }: { series: HistoryPoint[] }) {
// sellPrice é o PU indicativo de venda no dia — marcação a mercado.
const data = series
.filter((p) => p.sellPrice != null)
.map((p) => ({
date: p.baseDate,
sellPrice: p.sellPrice as number,
}));
if (data.length === 0) {
return <p className="text-gray-500">Sem dados no período.</p>;
}
const min = Math.min(...data.map((d) => d.sellPrice));
const max = Math.max(...data.map((d) => d.sellPrice));
const pad = (max - min) * 0.1;
return (
<div className="w-full h-80 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis
dataKey="date"
tickFormatter={(d) =>
new Date(d).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
})
}
minTickGap={32}
/>
<YAxis
domain={[min - pad, max + pad]}
tickFormatter={(v) => `R$ ${v.toFixed(0)}`}
/>
<Tooltip
formatter={(v: number) => [`R$ ${v.toFixed(2)}`, 'PU venda']}
labelFormatter={(d) => new Date(d).toLocaleDateString('pt-BR')}
/>
<Line
type="monotone"
dataKey="sellPrice"
stroke="#2563eb"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}Cada ponto da série tem esta forma. O exemplo foi tirado direto do endpoint
/api/v2/treasury/indicators/history:
{
"baseDate": "2026-02-02",
"buyRate": 13.56,
"sellRate": 13.68,
"buyPrice": 820.97,
"sellPrice": 815.06,
"basePrice": 815.06
}A série do Prefixado 2037 entre janeiro e maio de 2026 tem 70 pontos
(um por dia útil). O sellPrice cai de R$ 815 para R$ 809 no período.
Isso mostra a curva de juros abrindo. É justamente esse efeito que tem
nome de marcação a mercado. E o gráfico deixa ele visível.
Cuidados com o rateInfo
Tabelas que misturam vários indexadores na mesma coluna "taxa" caem numa armadilha. Olhe este pedaço real da resposta:
[
{
"symbol": "tesouro-selic-01032031",
"sellRate": 0.09,
"rateInfo": { "rateType": "spreadOverSelic" }
},
{
"symbol": "tesouro-prefixado-com-juros-semestrais-01012037",
"sellRate": 14.48,
"rateInfo": { "rateType": "nominalAnnualRate" }
},
{
"symbol": "tesouro-ipca-com-juros-semestrais-15082060",
"sellRate": 7.34,
"rateInfo": { "rateType": "realAnnualRateOverIpca" }
}
]Se você jogar o sellRate numa coluna chamada só "Taxa", o leitor vai
achar que o Tesouro Selic 2031 rende 0,09% ao ano. Isso é um desastre. O
helper formatRate do tutorial evita esse erro:
Regra de ouro
Nunca mostre buyRate ou sellRate sem olhar o rateInfo.rateType. O
campo rateInfo.description já vem com a explicação em português pronta.
Dá pra usar direto num tooltip.
// Exemplo de tooltip explicativo
<span title={bond.rateInfo.description} className="cursor-help">
{formatRate(bond.rateInfo, bond.sellRate)}
</span>app/loading.tsx: streaming
Server Components combinam bem com o arquivo loading.tsx. Ele vira um
limite de Suspense automático na raiz. Enquanto os dados não chegam, o
usuário já vê uma "casca" da página:
// app/loading.tsx
export default function Loading() {
return (
<main className="container mx-auto py-8 px-4 space-y-12">
<div className="h-10 w-72 bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="h-40 bg-gray-200 dark:bg-gray-800 rounded animate-pulse"
/>
))}
</div>
<div className="h-80 bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
<div className="h-96 bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
</main>
);
}Deploy na Vercel
É bem direto:
bun add -g vercel
vercel deployAgora, configure as duas variáveis de ambiente no painel da Vercel (Settings → Environment Variables):
| Variável | Valor |
|---|---|
BRAPI_TOKEN | seu token de produção da brapi.dev |
BRAPI_BASE_URL | https://brapi.dev/api/v2/treasury |
A brapi já entrega respostas no cache na borda. O tempo de vida é bom:
15 minutos para snapshot e mais que isso para o histórico. Combine isso
com o next.revalidate do nosso cliente. Você não precisa da biblioteca
de cache no cliente, nem de Redis, nem de useEffect. O HTML chega
pronto. O gráfico hidrata. Fim.
Cache em camadas
A resposta passa por várias camadas antes de chegar ao usuário. Olhe o fluxo:
Acima: o cache em camadas. Quando o usuário pede a página, a resposta quase sempre vem de uma camada perto dele. Por isso o tempo de resposta costuma ser de poucos milissegundos. Só de vez em quando o pedido vai até o Tesouro Transparente.
Invalidação manual
Quer forçar uma atualização (por exemplo, num webhook diário ao final do
pregão)? Crie uma Server Action que chama revalidateTag('treasury').
Todo fetch marcado em lib/brapi.ts será buscado de novo no próximo
acesso.
Próximos passos
Este Dashboard Tesouro Direto Next.js cobre o básico. Daqui dá pra evoluir em várias direções:
- Curva de juros prefixada — junte vários prefixados de vencimentos
diferentes. Plote
sellRateno eixo Y ematurityDateno eixo X (duração). O endpointlistcomindexer=prefixado&sortBy=maturityDateentrega tudo de uma vez só. - Comparador de títulos — monte uma página
/comparar?symbols=A,B,Ccom a funçãotreasuryIndicators(ela aceita até 20 símbolos por chamada). - Alertas de marcação — uma Server Action diária compara o último
sellPricedo histórico com um teto ou um piso. Se passar, dispara webhook. - Cruzamento com macro — coloque a série da Selic do endpoint
/api/v2/macrono mesmo gráfico. Você vê na hora como a Selic mexe nos preços dos títulos.
Quer ir mais fundo? Estes posts ajudam:
- Tesouro Direto: guia completo 2026 — como investir
- CDI histórico via API: benchmark de renda fixa 2026
- API de ações com React e Next.js
- API B3 com TypeScript: integração completa
A documentação da API de Tesouro Direto fica em brapi.dev/docs/tesouro-direto. O plano Pro libera os três endpoints deste tutorial e a base completa de títulos.
Pronto para construir o seu Dashboard Tesouro Direto Next.js? Crie sua conta na brapi.dev, faça upgrade para o plano Pro e coloque o dashboard no ar antes do café acabar.
