diff --git a/package.json b/package.json
index d5233d54a..e5180fddb 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,9 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.11.1",
+		"@types/koa": "^2.0.45",
+		"@types/koa-bodyparser": "^4.2.0",
+		"@types/koa-router": "^7.0.27",
 		"@types/kue": "^0.11.8",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
@@ -140,6 +143,8 @@
 		"is-url": "1.2.4",
 		"js-yaml": "3.11.0",
 		"jsdom": "11.7.0",
+		"koa": "^2.5.0",
+		"koa-router": "^7.4.0",
 		"kue": "0.11.6",
 		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
new file mode 100644
index 000000000..ed0311af9
--- /dev/null
+++ b/src/server/activitypub.ts
@@ -0,0 +1,142 @@
+import * as Router from 'koa-router';
+import { parseRequest } from 'http-signature';
+
+import { createHttp } from '../queue';
+import context from '../remote/activitypub/renderer/context';
+import render from '../remote/activitypub/renderer/note';
+import Note from '../models/note';
+import User, { isLocalUser } from '../models/user';
+import renderNote from '../remote/activitypub/renderer/note';
+import renderKey from '../remote/activitypub/renderer/key';
+import renderPerson from '../remote/activitypub/renderer/person';
+import renderOrderedCollection from '../remote/activitypub/renderer/ordered-collection';
+//import parseAcct from '../acct/parse';
+import config from '../config';
+
+// Init router
+const router = new Router();
+
+//#region Routing
+
+// inbox
+router.post('/users/:user/inbox', ctx => {
+	let signature;
+
+	ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature;
+
+	try {
+		signature = parseRequest(ctx.req);
+	} catch (e) {
+		ctx.status = 401;
+		return;
+	}
+
+	createHttp({
+		type: 'processInbox',
+		activity: ctx.request.body,
+		signature
+	}).save();
+
+	ctx.status = 202;
+});
+
+// note
+router.get('/notes/:note', async (ctx, next) => {
+	const accepted = ctx.accepts('html', 'application/activity+json', 'application/ld+json');
+	if (!['application/activity+json', 'application/ld+json'].includes(accepted as string)) {
+		next();
+		return;
+	}
+
+	const note = await Note.findOne({
+		_id: ctx.params.note
+	});
+
+	if (note === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const rendered = await render(note);
+	rendered['@context'] = context;
+
+	ctx.body = rendered;
+});
+
+// outbot
+router.get('/users/:user/outbox', async ctx => {
+	const userId = ctx.params.user;
+
+	const user = await User.findOne({ _id: userId });
+
+	if (user === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const notes = await Note.find({ userId: user._id }, {
+		limit: 10,
+		sort: { _id: -1 }
+	});
+
+	const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
+	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes);
+	rendered['@context'] = context;
+
+	ctx.body = rendered;
+});
+
+// publickey
+router.get('/users/:user/publickey', async ctx => {
+	const userId = ctx.params.user;
+
+	const user = await User.findOne({ _id: userId });
+
+	if (user === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	if (isLocalUser(user)) {
+		const rendered = renderKey(user);
+		rendered['@context'] = context;
+
+		ctx.body = rendered;
+	} else {
+		ctx.status = 400;
+	}
+});
+
+// user
+router.get('/users/:user', async ctx => {
+	const userId = ctx.params.user;
+
+	const user = await User.findOne({ _id: userId });
+
+	if (user === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const rendered = renderPerson(user);
+	rendered['@context'] = context;
+
+	ctx.body = rendered;
+});
+
+// follow form
+router.get('/authorize-follow', async ctx => {
+	/* TODO
+	const { username, host } = parseAcct(ctx.query.acct);
+	if (host === null) {
+		res.sendStatus(422);
+		return;
+	}
+
+	const finger = await request(`https://${host}`)
+	*/
+});
+
+//#endregion
+
+export default router;
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
deleted file mode 100644
index 643d2945b..000000000
--- a/src/server/activitypub/inbox.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import * as bodyParser from 'body-parser';
-import * as express from 'express';
-import { parseRequest } from 'http-signature';
-import { createHttp } from '../../queue';
-
-const app = express.Router();
-
-app.post('/users/:user/inbox', bodyParser.json({
-	type() {
-		return true;
-	}
-}), async (req, res) => {
-	let signature;
-
-	req.headers.authorization = 'Signature ' + req.headers.signature;
-
-	try {
-		signature = parseRequest(req);
-	} catch (exception) {
-		return res.sendStatus(401);
-	}
-
-	createHttp({
-		type: 'processInbox',
-		activity: req.body,
-		signature,
-	}).save();
-
-	return res.status(202).end();
-});
-
-export default app;
diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts
deleted file mode 100644
index 042579db9..000000000
--- a/src/server/activitypub/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as express from 'express';
-
-import user from './user';
-import inbox from './inbox';
-import outbox from './outbox';
-import publicKey from './publickey';
-import note from './note';
-
-const app = express();
-app.disable('x-powered-by');
-
-app.use(user);
-app.use(inbox);
-app.use(outbox);
-app.use(publicKey);
-app.use(note);
-
-export default app;
diff --git a/src/server/activitypub/note.ts b/src/server/activitypub/note.ts
deleted file mode 100644
index 1c2e695b8..000000000
--- a/src/server/activitypub/note.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as express from 'express';
-import context from '../../remote/activitypub/renderer/context';
-import render from '../../remote/activitypub/renderer/note';
-import Note from '../../models/note';
-
-const app = express.Router();
-
-app.get('/notes/:note', async (req, res, next) => {
-	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
-	if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
-		return next();
-	}
-
-	const note = await Note.findOne({
-		_id: req.params.note
-	});
-
-	if (note === null) {
-		return res.sendStatus(404);
-	}
-
-	const rendered = await render(note);
-	rendered['@context'] = context;
-
-	res.json(rendered);
-});
-
-export default app;
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
deleted file mode 100644
index 1c97c17a2..000000000
--- a/src/server/activitypub/outbox.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as express from 'express';
-import context from '../../remote/activitypub/renderer/context';
-import renderNote from '../../remote/activitypub/renderer/note';
-import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
-import config from '../../config';
-import Note from '../../models/note';
-import User from '../../models/user';
-
-const app = express.Router();
-
-app.get('/users/:user/outbox', async (req, res) => {
-	const userId = req.params.user;
-
-	const user = await User.findOne({ _id: userId });
-
-	const notes = await Note.find({ userId: user._id }, {
-		limit: 20,
-		sort: { _id: -1 }
-	});
-
-	const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
-	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes);
-	rendered['@context'] = context;
-
-	res.json(rendered);
-});
-
-export default app;
diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts
deleted file mode 100644
index e874b8272..000000000
--- a/src/server/activitypub/publickey.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as express from 'express';
-import context from '../../remote/activitypub/renderer/context';
-import render from '../../remote/activitypub/renderer/key';
-import User, { isLocalUser } from '../../models/user';
-
-const app = express.Router();
-
-app.get('/users/:user/publickey', async (req, res) => {
-	const userId = req.params.user;
-
-	const user = await User.findOne({ _id: userId });
-
-	if (isLocalUser(user)) {
-		const rendered = render(user);
-		rendered['@context'] = context;
-
-		res.json(rendered);
-	} else {
-		res.sendStatus(400);
-	}
-});
-
-export default app;
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
deleted file mode 100644
index 9e98e92b6..000000000
--- a/src/server/activitypub/user.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as express from 'express';
-import context from '../../remote/activitypub/renderer/context';
-import render from '../../remote/activitypub/renderer/person';
-import User from '../../models/user';
-
-const app = express.Router();
-
-app.get('/users/:user', async (req, res) => {
-	const userId = req.params.user;
-
-	const user = await User.findOne({ _id: userId });
-
-	const rendered = render(user);
-	rendered['@context'] = context;
-
-	res.json(rendered);
-});
-
-export default app;
diff --git a/src/server/index.ts b/src/server/index.ts
index 962d3b5f4..e9bfa9e10 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5,67 +5,40 @@
 import * as fs from 'fs';
 import * as http from 'http';
 import * as https from 'https';
-import * as express from 'express';
-import * as morgan from 'morgan';
+import * as Koa from 'koa';
+import * as Router from 'koa-router';
+import * as bodyParser from 'koa-bodyparser';
 
 import activityPub from './activitypub';
 import webFinger from './webfinger';
-import log from './log-request';
 import config from '../config';
 
-/**
- * Init app
- */
-const app = express();
-app.disable('x-powered-by');
-app.set('trust proxy', 'loopback');
+// Init server
+const app = new Koa();
+app.proxy = true;
+app.use(bodyParser);
 
-app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', {
-	// create a write stream (in append mode)
-	stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null
-}));
-
-app.use((req, res, next) => {
-	log(req);
-	next();
-});
-
-/**
- * HSTS
- * 6month(15552000sec)
- */
+// HSTS
+// 6months (15552000sec)
 if (config.url.startsWith('https')) {
-	app.use((req, res, next) => {
-		res.header('strict-transport-security', 'max-age=15552000; preload');
+	app.use((ctx, next) => {
+		ctx.set('strict-transport-security', 'max-age=15552000; preload');
 		next();
 	});
 }
 
-// Drop request when without 'Host' header
-app.use((req, res, next) => {
-	if (!req.headers['host']) {
-		res.sendStatus(400);
-	} else {
-		next();
-	}
-});
+// Init router
+const router = new Router();
 
-// 互換性のため
-app.post('/meta', (req, res) => {
-	res.header('Access-Control-Allow-Origin', '*');
-	res.json({
-		version: 'nighthike'
-	});
-});
+// Routing
+router.use('/api', require('./api'));
+router.use('/files', require('./file'));
+router.use(activityPub.routes());
+router.use(webFinger.routes());
+router.use(require('./web'));
 
-/**
- * Register modules
- */
-app.use('/api', require('./api'));
-app.use('/files', require('./file'));
-app.use(activityPub);
-app.use(webFinger);
-app.use(require('./web'));
+// Register router
+app.use(router.routes());
 
 function createServer() {
 	if (config.https) {
diff --git a/src/server/log-request.ts b/src/server/log-request.ts
deleted file mode 100644
index e431aa271..000000000
--- a/src/server/log-request.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as crypto from 'crypto';
-import * as express from 'express';
-import * as proxyAddr from 'proxy-addr';
-import Xev from 'xev';
-
-const ev = new Xev();
-
-export default function(req: express.Request) {
-	const ip = proxyAddr(req, () => true);
-
-	const md5 = crypto.createHash('md5');
-	md5.update(ip);
-	const hashedIp = md5.digest('hex').substr(0, 3);
-
-	ev.emit('request', {
-		ip: hashedIp,
-		method: req.method,
-		hostname: req.hostname,
-		path: req.originalUrl
-	});
-}
diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
index dbf0999f3..e72592351 100644
--- a/src/server/webfinger.ts
+++ b/src/server/webfinger.ts
@@ -1,17 +1,19 @@
-import * as express from 'express';
+import * as Router from 'koa-router';
 
 import config from '../config';
 import parseAcct from '../acct/parse';
 import User from '../models/user';
 
-const app = express.Router();
+// Init router
+const router = new Router();
 
-app.get('/.well-known/webfinger', async (req, res) => {
-	if (typeof req.query.resource !== 'string') {
-		return res.sendStatus(400);
+router.get('/.well-known/webfinger', async ctx => {
+	if (typeof ctx.query.resource !== 'string') {
+		ctx.status = 400;
+		return;
 	}
 
-	const resourceLower = req.query.resource.toLowerCase();
+	const resourceLower = ctx.query.resource.toLowerCase();
 	const webPrefix = config.url.toLowerCase() + '/@';
 	let acctLower;
 
@@ -25,15 +27,21 @@ app.get('/.well-known/webfinger', async (req, res) => {
 
 	const parsedAcctLower = parseAcct(acctLower);
 	if (![null, config.host.toLowerCase()].includes(parsedAcctLower.host)) {
-		return res.sendStatus(422);
+		ctx.status = 422;
+		return;
 	}
 
-	const user = await User.findOne({ usernameLower: parsedAcctLower.username, host: null });
+	const user = await User.findOne({
+		usernameLower: parsedAcctLower.username,
+		host: null
+	});
+
 	if (user === null) {
-		return res.sendStatus(404);
+		ctx.status = 404;
+		return;
 	}
 
-	return res.json({
+	ctx.body = {
 		subject: `acct:${user.username}@${config.host}`,
 		links: [{
 			rel: 'self',
@@ -47,7 +55,7 @@ app.get('/.well-known/webfinger', async (req, res) => {
 			rel: 'http://ostatus.org/schema/1.0/subscribe',
 			template: `${config.url}/authorize-follow?acct={uri}`
 		}]
-	});
+	};
 });
 
-export default app;
+export default router;