Hero
Back to all guides

How to use JKT48Connect with Hono

Build a JKT48 data API server with Hono and JKT48Connect. Serve member info, theater schedules, events, and news through your own endpoints using Bun or Node.js.

JKT48Connect Team

3/20/2026

Updated on 3/20/2026

Beginner
10 min

How to use JKT48Connect with Hono

Hono is a fast, lightweight web framework that runs on Bun, Node.js, Cloudflare Workers, and more. Combining it with JKT48Connect lets you build your own JKT48 data API server — proxying and transforming member info, theater schedules, events, and news through your own endpoints in a clean, type-safe way.

This is useful when you want to add caching, authentication, rate limiting, or custom response shaping on top of the JKT48Connect REST API before it reaches your frontend or bot.

Prerequisites

Install Hono and set up the project

Create a new project and install Hono. If you're using Bun:

mkdir jkt48-hono && cd jkt48-hono
bun init -y
bun add hono

If you prefer Node.js:

mkdir jkt48-hono && cd jkt48-hono
npm init -y
npm install hono @hono/node-server

Store your API key in a .env file:

.env
JKT48_API_KEY=your-api-key-here

Create the JKT48Connect client

Create a reusable client module that wraps all fetch calls to the JKT48Connect API. This keeps your route handlers clean and gives you a single place to swap the base URL or add headers later.

src/jkt48.ts
const BASE_URL = 'https://v2.jkt48connect.com';
const API_KEY = process.env.JKT48_API_KEY!;

if (!API_KEY) {
  throw new Error('JKT48_API_KEY environment variable is not set');
}

async function jkt48Fetch<T>(path: string, params?: Record<string, string>): Promise<T> {
  const url = new URL(`${BASE_URL}${path}`);
  url.searchParams.set('apikey', API_KEY);

  if (params) {
    for (const [key, value] of Object.entries(params)) {
      url.searchParams.set(key, value);
    }
  }

  const res = await fetch(url.toString());

  if (!res.ok) {
    throw new Error(`JKT48Connect error: HTTP ${res.status} on ${path}`);
  }

  return res.json() as Promise<T>;
}

export const jkt48 = {
  members: () =>
    jkt48Fetch<object[]>('/api/jkt48/members'),

  memberDetail: (url: string) =>
    jkt48Fetch<object>('/api/jkt48/member-detail', { url }),

  theater: (page = '1', perpage = '10') =>
    jkt48Fetch<object>('/api/jkt48/theater', { page, perpage }),

  events: () =>
    jkt48Fetch<object[]>('/api/jkt48/events'),

  news: (page = '1', perpage = '10') =>
    jkt48Fetch<object>('/api/jkt48/news', { page, perpage }),

  newsDetail: (id: string) =>
    jkt48Fetch<object>('/api/jkt48/news-detail', { id }),

  birthday: () =>
    jkt48Fetch<object[]>('/api/jkt48/birthday'),
};

Add route handlers

Set up the Hono app and register your routes. Each route calls the matching method on the jkt48 client and returns the result as JSON.

Bun entry point

src/index.ts
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { jkt48 } from './jkt48';

const app = new Hono();

app.use('*', logger());

// Health check
app.get('/', (c) => c.json({ status: true, message: 'JKT48 Hono API' }));

export default {
  port: 3000,
  fetch: app.fetch,
};

Node.js entry point

src/index.ts
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { serve } from '@hono/node-server';
import { jkt48 } from './jkt48';

const app = new Hono();

app.use('*', logger());

app.get('/', (c) => c.json({ status: true, message: 'JKT48 Hono API' }));

serve({ fetch: app.fetch, port: 3000 }, () => {
  console.log('Server running on http://localhost:3000');
});

Serve members, theater, and events

Add individual route files for each resource, then mount them on the app.

Members

src/routes/members.ts
import { Hono } from 'hono';
import { jkt48 } from '../jkt48';

const members = new Hono();

// GET /members — full roster
members.get('/', async (c) => {
  const data = await jkt48.members();
  return c.json(data);
});

// GET /members/active — non-graduated members only
members.get('/active', async (c) => {
  const data = await jkt48.members() as any[];
  const active = data.filter((m) => !m.is_graduate);
  return c.json(active);
});

// GET /members/:url — single member detail
members.get('/:url', async (c) => {
  const url = c.req.param('url');
  const data = await jkt48.memberDetail(url);
  return c.json(data);
});

export default members;

Theater

src/routes/theater.ts
import { Hono } from 'hono';
import { jkt48 } from '../jkt48';

const theater = new Hono();

// GET /theater?page=1&perpage=10
theater.get('/', async (c) => {
  const page = c.req.query('page') ?? '1';
  const perpage = c.req.query('perpage') ?? '10';
  const data = await jkt48.theater(page, perpage);
  return c.json(data);
});

// GET /theater/today — shows happening today
theater.get('/today', async (c) => {
  const data = await jkt48.theater('1', '50') as any;
  const today = new Date().toDateString();
  const todayShows = data.theater.filter(
    (show: any) => new Date(show.date).toDateString() === today
  );
  return c.json({ shows: todayShows, count: todayShows.length });
});

// GET /theater/seitansai — shows with birthday celebrations
theater.get('/seitansai', async (c) => {
  const data = await jkt48.theater('1', '50') as any;
  const seitansaiShows = data.theater.filter(
    (show: any) => show.seitansai && show.seitansai.length > 0
  );
  return c.json({ shows: seitansaiShows, count: seitansaiShows.length });
});

export default theater;

Events

src/routes/events.ts
import { Hono } from 'hono';
import { jkt48 } from '../jkt48';

const events = new Hono();

// GET /events — all upcoming events
events.get('/', async (c) => {
  const data = await jkt48.events();
  return c.json(data);
});

// GET /events/week — events in the next 7 days
events.get('/week', async (c) => {
  const data = await jkt48.events() as any[];
  const now = new Date();
  const weekLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);

  const thisWeek = data.filter((event) => {
    const eventDate = new Date(event.date);
    return eventDate >= now && eventDate <= weekLater;
  });

  return c.json({ events: thisWeek, count: thisWeek.length });
});

export default events;

News

src/routes/news.ts
import { Hono } from 'hono';
import { jkt48 } from '../jkt48';

const news = new Hono();

// GET /news?page=1&perpage=10
news.get('/', async (c) => {
  const page = c.req.query('page') ?? '1';
  const perpage = c.req.query('perpage') ?? '10';
  const data = await jkt48.news(page, perpage);
  return c.json(data);
});

// GET /news/:id — single article detail
news.get('/:id', async (c) => {
  const id = c.req.param('id');
  const data = await jkt48.newsDetail(id);
  return c.json(data);
});

export default news;

Mount all routes

src/index.ts
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import members from './routes/members';
import theater from './routes/theater';
import events from './routes/events';
import news from './routes/news';
import { jkt48 } from './jkt48';

const app = new Hono();

app.use('*', logger());

app.get('/', (c) => c.json({ status: true, message: 'JKT48 Hono API' }));

app.route('/members', members);
app.route('/theater', theater);
app.route('/events', events);
app.route('/news', news);

// Birthday shortcut
app.get('/birthday', async (c) => {
  const data = await jkt48.birthday();
  return c.json(data);
});

export default {
  port: 3000,
  fetch: app.fetch,
};

Handle errors gracefully

Add a global error handler so any unhandled exception from the JKT48Connect client returns a clean JSON response instead of crashing the server.

src/index.ts
import { HTTPException } from 'hono/http-exception';

// Catch HTTP exceptions (e.g. from c.notFound())
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json({ status: false, message: err.message }, err.status);
  }

  console.error(err);
  return c.json({ status: false, message: 'Internal server error' }, 500);
});

// 404 fallback
app.notFound((c) =>
  c.json({ status: false, message: `Route ${c.req.path} not found` }, 404)
);

For route-level error handling, wrap JKT48Connect calls in try/catch and return a structured error:

members.get('/:url', async (c) => {
  try {
    const url = c.req.param('url');
    const data = await jkt48.memberDetail(url);
    return c.json(data);
  } catch (err) {
    return c.json({ status: false, message: 'Member not found' }, 404);
  }
});

Verify your setup

Start the server:

# Bun
bun run src/index.ts

# Node.js
npx tsx src/index.ts

Then test each endpoint with curl or your browser:

curl http://localhost:3000/members/active
curl http://localhost:3000/theater/today
curl http://localhost:3000/events/week
curl http://localhost:3000/news?page=1&perpage=5
curl http://localhost:3000/birthday

You should receive JSON responses from the JKT48Connect API routed through your Hono server. Open the terminal running the server and you'll see each request logged by the Hono logger middleware.

If you get a JKT48_API_KEY environment variable is not set error, make sure your .env file is present and your runtime is loading it. With Bun, .env is loaded automatically. With Node.js, add --env-file=.env to your start command or use a package like dotenv.

Next steps

The JKT48Connect API docs lists all available endpoints including IDN Live, SHOWROOM Live, YouTube Live, recent live streams, video call schedules, and chat streams. You can add each one as a new route following the same pattern in this guide. For deploying your Hono server, see the Hono deployment docs for guides on Cloudflare Workers, Vercel, Railway, and Fly.io.

Loved by JKT48 developers everywhere

From personal fansites to large fan communities, JKT48Connect helps developers build better JKT48 apps — without the scraping headache.

  • Rizky Pratama
    Rizky Pratama
    @rizkypratama_dev

    Baru integrasi @JKT48Connect ke fansite aku dan literally took less than 10 menit buat dapetin data member + jadwal teater sekaligus.

    No more scraping yang sering break. Ini yang selama ini dicari! 🔥

  • Fadhil Hakim
    Fadhil Hakim
    @fadhildev48

    JKT48Connect is a beast. Data member lengkap, live stream real-time, theater schedule — all in one API.

    Dulu habis waktu berhari-hari bikin scraper. Sekarang cukup satu npm install.

  • Alicia Ramadhani
    Alicia Ramadhani
    @alicia_wota

    Sebagai fans yang juga developer, JKT48Connect benar-benar game changer.

    Birthday API-nya keren banget — bisa langsung bikin reminder ulang tahun oshi tanpa hitung manual. Dokumentasinya juga sangat jelas ✨

  • Bintang Nugroho
    Bintang Nugroho
    @bintang_codes

    Discord bot JKT48 aku sekarang pakai JKT48Connect dan hasilnya jauh lebih stable.

    Webhook-nya works perfectly, langsung notif ke server kalau member lagi live di IDN.

  • Kavya Indraswari
    Kavya Indraswari
    @kavya_jkt48fan

    🤯 Baru deploy fansite pakai @JKT48Connect dan dalam 1 jam udah ada data member dari semua generasi, jadwal show minggu ini, dan live stream tracking.

    TypeScript SDK-nya autocomplete semua endpoint. Ini yang namanya developer experience! #jkt48 #webdev

  • Dimas Aryanto
    Dimas Aryanto
    @dimas_wr

    Udah 3 bulan pakai JKT48Connect buat app tracker oshi aku.

    Belum pernah sekalipun data-nya salah atau API-nya down pas lagi dibutuhkan.

    Worth every rupiah. 🙌

  • Naufal Keanu
    Naufal Keanu
    @naufalkeanu

    Fansite JKT48 tim kami sekarang fully powered by @JKT48Connect.

    Dari yang taunya scraping manual, sekarang data theater, live stream, dan news semua real-time.

    Kalau kamu mau bikin sesuatu tentang JKT48, ini wajib dipakai 🚀

Ready to build something for JKT48?
Start integrating in minutes

Join developers and fanbases using JKT48Connect. Get your API key for free and start accessing real-time JKT48 data today.

Read the docs