diff --git a/packages/core/src/routes/application.ts b/packages/core/src/routes/application.ts
index c6c5bc596..ccbfd70c4 100644
--- a/packages/core/src/routes/application.ts
+++ b/packages/core/src/routes/application.ts
@@ -1,5 +1,4 @@
 import { Applications } from '@logto/schemas';
-import Router from 'koa-router';
 import { object, string } from 'zod';
 
 import koaGuard from '@/middleware/koa-guard';
@@ -11,9 +10,11 @@ import {
 } from '@/queries/application';
 import { buildIdGenerator } from '@/utils/id';
 
+import { AuthedRouter } from './types';
+
 const applicationId = buildIdGenerator(21);
 
-export default function applicationRoutes<StateT, ContextT>(router: Router<StateT, ContextT>) {
+export default function applicationRoutes<T extends AuthedRouter>(router: T) {
   router.post(
     '/application',
     koaGuard({
diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts
index 4cb467831..07a095870 100644
--- a/packages/core/src/routes/init.ts
+++ b/packages/core/src/routes/init.ts
@@ -3,20 +3,22 @@ import mount from 'koa-mount';
 import Router from 'koa-router';
 import { Provider } from 'oidc-provider';
 
-import koaAuth, { WithAuthContext } from '@/middleware/koa-auth';
+import koaAuth from '@/middleware/koa-auth';
 import applicationRoutes from '@/routes/application';
 import sessionRoutes from '@/routes/session';
 import swaggerRoutes from '@/routes/swagger';
 import userRoutes from '@/routes/user';
 
+import { AnonymousRouter, AuthedRouter } from './types';
+
 const createRouters = (provider: Provider) => {
-  const anonymousRouter = new Router();
+  const anonymousRouter: AnonymousRouter = new Router();
 
   sessionRoutes(anonymousRouter, provider);
   userRoutes(anonymousRouter);
   swaggerRoutes(anonymousRouter);
 
-  const router = new Router<unknown, WithAuthContext>();
+  const router: AuthedRouter = new Router();
   router.use(koaAuth());
   applicationRoutes(router);
 
diff --git a/packages/core/src/routes/session.ts b/packages/core/src/routes/session.ts
index 2493436fd..855ce6e29 100644
--- a/packages/core/src/routes/session.ts
+++ b/packages/core/src/routes/session.ts
@@ -1,6 +1,5 @@
 import { conditional } from '@logto/essentials';
 import { LogtoErrorCode } from '@logto/phrases';
-import Router from 'koa-router';
 import { Provider } from 'oidc-provider';
 import { object, string } from 'zod';
 
@@ -10,7 +9,9 @@ import { findUserByUsername } from '@/queries/user';
 import assert from '@/utils/assert';
 import { encryptPassword } from '@/utils/password';
 
-export default function sessionRoutes(router: Router, provider: Provider) {
+import { AnonymousRouter } from './types';
+
+export default function sessionRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
   router.post(
     '/session',
     koaGuard({ body: object({ username: string().optional(), password: string().optional() }) }),
diff --git a/packages/core/src/routes/swagger.ts b/packages/core/src/routes/swagger.ts
index 65e72df48..249bd296f 100644
--- a/packages/core/src/routes/swagger.ts
+++ b/packages/core/src/routes/swagger.ts
@@ -1,11 +1,13 @@
-import Router, { IMiddleware } from 'koa-router';
+import { IMiddleware } from 'koa-router';
 import { OpenAPIV3 } from 'openapi-types';
 
 import { isGuardMiddleware, WithGuardConfig } from '@/middleware/koa-guard';
 import { toTitle } from '@/utils/string';
 import { zodTypeToSwagger } from '@/utils/zod';
 
-export default function swaggerRoutes(router: Router) {
+import { AnonymousRouter } from './types';
+
+export default function swaggerRoutes<T extends AnonymousRouter>(router: T) {
   router.get('/swagger.json', async (ctx, next) => {
     const routes = ctx.router.stack.map(({ path, stack, methods }) => {
       const guard = stack.find((function_): function_ is WithGuardConfig<IMiddleware> =>
diff --git a/packages/core/src/routes/types.ts b/packages/core/src/routes/types.ts
new file mode 100644
index 000000000..cdd727fb8
--- /dev/null
+++ b/packages/core/src/routes/types.ts
@@ -0,0 +1,7 @@
+import Router from 'koa-router';
+
+import { WithAuthContext } from '@/middleware/koa-auth';
+import { WithI18nContext } from '@/middleware/koa-i18next';
+
+export type AnonymousRouter = Router<unknown, WithI18nContext>;
+export type AuthedRouter = Router<unknown, WithAuthContext<WithI18nContext>>;
diff --git a/packages/core/src/routes/user.ts b/packages/core/src/routes/user.ts
index c7e2a306f..622f77ce3 100644
--- a/packages/core/src/routes/user.ts
+++ b/packages/core/src/routes/user.ts
@@ -1,5 +1,4 @@
 import { PasswordEncryptionMethod } from '@logto/schemas';
-import Router from 'koa-router';
 import { nanoid } from 'nanoid';
 import { object, string } from 'zod';
 
@@ -9,6 +8,8 @@ import { hasUser, hasUserWithId, insertUser } from '@/queries/user';
 import { buildIdGenerator } from '@/utils/id';
 import { encryptPassword } from '@/utils/password';
 
+import { AnonymousRouter } from './types';
+
 const userId = buildIdGenerator(12);
 
 const generateUserId = async (maxRetries = 500) => {
@@ -23,7 +24,7 @@ const generateUserId = async (maxRetries = 500) => {
   throw new Error('Cannot generate user ID in reasonable retries');
 };
 
-export default function userRoutes(router: Router) {
+export default function userRoutes<T extends AnonymousRouter>(router: T) {
   router.post(
     '/user',
     koaGuard({