summary refs log tree commit diff
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/middlewares/Authentication.ts9
-rw-r--r--src/api/middlewares/RateLimit.ts26
-rw-r--r--src/api/middlewares/Translation.ts2
-rw-r--r--src/api/routes/applications/#id/bot/index.ts117
-rw-r--r--src/api/routes/applications/#id/entitlements.ts22
-rw-r--r--src/api/routes/applications/#id/index.ts91
-rw-r--r--src/api/routes/applications/#id/skus.ts18
-rw-r--r--src/api/routes/applications/detectable.ts20
-rw-r--r--src/api/routes/applications/index.ts45
-rw-r--r--src/api/routes/auth/forgot.ts13
-rw-r--r--src/api/routes/auth/generate-registration-tokens.ts19
-rw-r--r--src/api/routes/auth/location-metadata.ts35
-rw-r--r--src/api/routes/auth/login.ts12
-rw-r--r--src/api/routes/auth/logout.ts33
-rw-r--r--src/api/routes/auth/mfa/totp.ts18
-rw-r--r--src/api/routes/auth/mfa/webauthn.ts8
-rw-r--r--src/api/routes/auth/register.ts46
-rw-r--r--src/api/routes/auth/reset.ts18
-rw-r--r--src/api/routes/auth/verify/index.ts16
-rw-r--r--src/api/routes/auth/verify/resend.ts13
-rw-r--r--src/api/routes/auth/verify/view-backup-codes-challenge.ts12
-rw-r--r--src/api/routes/channels/#channel_id/index.ts40
-rw-r--r--src/api/routes/channels/#channel_id/invites.ts36
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/ack.ts10
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts11
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/index.ts125
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts66
-rw-r--r--src/api/routes/channels/#channel_id/messages/bulk-delete.ts18
-rw-r--r--src/api/routes/channels/#channel_id/messages/index.ts235
-rw-r--r--src/api/routes/channels/#channel_id/permissions.ts14
-rw-r--r--src/api/routes/channels/#channel_id/pins.ts42
-rw-r--r--src/api/routes/channels/#channel_id/purge.ts22
-rw-r--r--src/api/routes/channels/#channel_id/recipients.ts142
-rw-r--r--src/api/routes/channels/#channel_id/typing.ts13
-rw-r--r--src/api/routes/channels/#channel_id/webhooks.ts42
-rw-r--r--src/api/routes/connections/#connection_name/#connection_id/refresh.ts2
-rw-r--r--src/api/routes/connections/#connection_name/callback.ts2
-rw-r--r--src/api/routes/discoverable-guilds.ts84
-rw-r--r--src/api/routes/discovery.ts34
-rw-r--r--src/api/routes/download.ts47
-rw-r--r--src/api/routes/gateway/bot.ts44
-rw-r--r--src/api/routes/gateway/index.ts30
-rw-r--r--src/api/routes/gifs/search.ts68
-rw-r--r--src/api/routes/gifs/trending-gifs.ts63
-rw-r--r--src/api/routes/gifs/trending.ts180
-rw-r--r--src/api/routes/guild-recommendations.ts52
-rw-r--r--src/api/routes/guilds/#guild_id/bans.ts85
-rw-r--r--src/api/routes/guilds/#guild_id/channels.ts58
-rw-r--r--src/api/routes/guilds/#guild_id/delete.ts60
-rw-r--r--src/api/routes/guilds/#guild_id/discovery-requirements.ts70
-rw-r--r--src/api/routes/guilds/#guild_id/emojis.ts110
-rw-r--r--src/api/routes/guilds/#guild_id/index.ts159
-rw-r--r--src/api/routes/guilds/#guild_id/invites.ts11
-rw-r--r--src/api/routes/guilds/#guild_id/member-verification.ts26
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/index.ts212
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/nick.ts15
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts20
-rw-r--r--src/api/routes/guilds/#guild_id/members/index.ts67
-rw-r--r--src/api/routes/guilds/#guild_id/messages/search.ts253
-rw-r--r--src/api/routes/guilds/#guild_id/profile/index.ts15
-rw-r--r--src/api/routes/guilds/#guild_id/prune.ts55
-rw-r--r--src/api/routes/guilds/#guild_id/regions.ts37
-rw-r--r--src/api/routes/guilds/#guild_id/roles/#role_id/index.ts77
-rw-r--r--src/api/routes/guilds/#guild_id/roles/index.ts50
-rw-r--r--src/api/routes/guilds/#guild_id/stickers.ts100
-rw-r--r--src/api/routes/guilds/#guild_id/templates.ts73
-rw-r--r--src/api/routes/guilds/#guild_id/vanity-url.ts46
-rw-r--r--src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts18
-rw-r--r--src/api/routes/guilds/#guild_id/welcome-screen.ts71
-rw-r--r--src/api/routes/guilds/#guild_id/widget.json.ts143
-rw-r--r--src/api/routes/guilds/#guild_id/widget.png.ts294
-rw-r--r--src/api/routes/guilds/#guild_id/widget.ts49
-rw-r--r--src/api/routes/guilds/index.ts28
-rw-r--r--src/api/routes/guilds/templates/index.ts119
-rw-r--r--src/api/routes/invites/index.ts125
-rw-r--r--src/api/routes/oauth2/authorize.ts234
-rw-r--r--src/api/routes/ping.ts44
-rw-r--r--src/api/routes/policies/instance/domains.ts41
-rw-r--r--src/api/routes/policies/instance/index.ts20
-rw-r--r--src/api/routes/policies/instance/limits.ts20
-rw-r--r--src/api/routes/policies/stats.ts41
-rw-r--r--src/api/routes/read-states/ack-bulk.ts12
-rw-r--r--src/api/routes/science.ts18
-rw-r--r--src/api/routes/sticker-packs/index.ts22
-rw-r--r--src/api/routes/stickers/#sticker_id/index.ts22
-rw-r--r--src/api/routes/stop.ts12
-rw-r--r--src/api/routes/updates.ts62
-rw-r--r--src/api/routes/users/#id/delete.ts13
-rw-r--r--src/api/routes/users/#id/index.ts22
-rw-r--r--src/api/routes/users/#id/profile.ts54
-rw-r--r--src/api/routes/users/#id/relationships.ts16
-rw-r--r--src/api/routes/users/@me/channels.ts51
-rw-r--r--src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts2
-rw-r--r--src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts2
-rw-r--r--src/api/routes/users/@me/delete.ts69
-rw-r--r--src/api/routes/users/@me/disable.ts61
-rw-r--r--src/api/routes/users/@me/guilds.ts133
-rw-r--r--src/api/routes/users/@me/guilds/#guild_id/settings.ts40
-rw-r--r--src/api/routes/users/@me/index.ts59
-rw-r--r--src/api/routes/users/@me/mfa/codes-verification.ts21
-rw-r--r--src/api/routes/users/@me/mfa/codes.ts22
-rw-r--r--src/api/routes/users/@me/mfa/totp/disable.ts22
-rw-r--r--src/api/routes/users/@me/mfa/totp/enable.ts23
-rw-r--r--src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts34
-rw-r--r--src/api/routes/users/@me/mfa/webauthn/credentials/index.ts12
-rw-r--r--src/api/routes/users/@me/notes.ts134
-rw-r--r--src/api/routes/users/@me/relationships.ts195
-rw-r--r--src/api/routes/users/@me/settings.ts46
-rw-r--r--src/api/routes/voice/regions.ts21
-rw-r--r--src/api/util/handlers/route.ts51
-rw-r--r--src/api/util/utility/ipAddress.ts2
111 files changed, 4052 insertions, 2062 deletions
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index d0e4d8a0..9e41b453 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -16,7 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { checkToken, Config, Rights } from "@spacebar/util";
+import { checkToken, Rights } from "@spacebar/util";
 import * as Sentry from "@sentry/node";
 import { NextFunction, Request, Response } from "express";
 import { HTTPError } from "lambert-server";
@@ -92,12 +92,7 @@ export async function Authentication(
 	Sentry.setUser({ id: req.user_id });
 
 	try {
-		const { jwtSecret } = Config.get().security;
-
-		const { decoded, user } = await checkToken(
-			req.headers.authorization,
-			jwtSecret,
-		);
+		const { decoded, user } = await checkToken(req.headers.authorization);
 
 		req.token = decoded;
 		req.user_id = decoded.id;
diff --git a/src/api/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts
index 0da292e9..f5bfbb4f 100644
--- a/src/api/middlewares/RateLimit.ts
+++ b/src/api/middlewares/RateLimit.ts
@@ -83,6 +83,13 @@ export default function rateLimit(opts: {
 
 		const offender = Cache.get(executor_id + bucket_id);
 
+		res.set("X-RateLimit-Limit", `${max_hits}`)
+			.set("X-RateLimit-Remaining", `${max_hits - (offender?.hits || 0)}`)
+			.set("X-RateLimit-Bucket", `${bucket_id}`)
+			// assuming we aren't blocked, a new window will start after this request
+			.set("X-RateLimit-Reset", `${Date.now() + opts.window}`)
+			.set("X-RateLimit-Reset-After", `${opts.window}`);
+
 		if (offender) {
 			let reset = offender.expires_at.getTime();
 			let resetAfterMs = reset - Date.now();
@@ -96,6 +103,12 @@ export default function rateLimit(opts: {
 				Cache.delete(executor_id + bucket_id);
 			}
 
+			res.set("X-RateLimit-Reset", `${reset}`);
+			res.set(
+				"X-RateLimit-Reset-After",
+				`${Math.max(0, Math.ceil(resetAfterSec))}`,
+			);
+
 			if (offender.blocked) {
 				const global = bucket_id === "global";
 				// each block violation pushes the expiry one full window further
@@ -109,16 +122,17 @@ export default function rateLimit(opts: {
 				console.log(`blocked bucket: ${bucket_id} ${executor_id}`, {
 					resetAfterMs,
 				});
+
+				if (global) res.set("X-RateLimit-Global", "true");
+
 				return (
 					res
 						.status(429)
-						.set("X-RateLimit-Limit", `${max_hits}`)
 						.set("X-RateLimit-Remaining", "0")
-						.set("X-RateLimit-Reset", `${reset}`)
-						.set("X-RateLimit-Reset-After", `${resetAfterSec}`)
-						.set("X-RateLimit-Global", `${global}`)
-						.set("Retry-After", `${Math.ceil(resetAfterSec)}`)
-						.set("X-RateLimit-Bucket", `${bucket_id}`)
+						.set(
+							"Retry-After",
+							`${Math.max(0, Math.ceil(resetAfterSec))}`,
+						)
 						// TODO: error rate limit message translation
 						.send({
 							message: "You are being rate limited.",
diff --git a/src/api/middlewares/Translation.ts b/src/api/middlewares/Translation.ts
index 60ff4ad7..f3a4c8df 100644
--- a/src/api/middlewares/Translation.ts
+++ b/src/api/middlewares/Translation.ts
@@ -20,7 +20,7 @@ import fs from "fs";
 import path from "path";
 import i18next from "i18next";
 import i18nextMiddleware from "i18next-http-middleware";
-import i18nextBackend from "i18next-node-fs-backend";
+import i18nextBackend from "i18next-fs-backend";
 import { Router } from "express";
 
 const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
diff --git a/src/api/routes/applications/#id/bot/index.ts b/src/api/routes/applications/#id/bot/index.ts
index e3f1832c..3c431e3d 100644
--- a/src/api/routes/applications/#id/bot/index.ts
+++ b/src/api/routes/applications/#id/bot/index.ts
@@ -16,78 +16,99 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
 import {
 	Application,
-	generateToken,
-	User,
 	BotModifySchema,
-	handleFile,
 	DiscordApiErrors,
+	User,
+	createAppBotUser,
+	generateToken,
+	handleFile,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { verifyToken } from "node-2fa";
 
 const router: Router = Router();
 
-router.post("/", route({}), async (req: Request, res: Response) => {
-	const app = await Application.findOneOrFail({
-		where: { id: req.params.id },
-		relations: ["owner"],
-	});
-
-	if (app.owner.id != req.user_id)
-		throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
-
-	const user = await User.register({
-		username: app.name,
-		password: undefined,
-		id: app.id,
-		req,
-	});
-
-	user.id = app.id;
-	user.premium_since = new Date();
-	user.bot = true;
-
-	await user.save();
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {
+				body: "TokenOnlyResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const app = await Application.findOneOrFail({
+			where: { id: req.params.id },
+			relations: ["owner"],
+		});
 
-	// flags is NaN here?
-	app.assign({ bot: user, flags: app.flags || 0 });
+		if (app.owner.id != req.user_id)
+			throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
 
-	await app.save();
+		const user = await createAppBotUser(app, req);
 
-	res.send({
-		token: await generateToken(user.id),
-	}).status(204);
-});
+		res.send({
+			token: await generateToken(user.id),
+		}).status(204);
+	},
+);
 
-router.post("/reset", route({}), async (req: Request, res: Response) => {
-	const bot = await User.findOneOrFail({ where: { id: req.params.id } });
-	const owner = await User.findOneOrFail({ where: { id: req.user_id } });
+router.post(
+	"/reset",
+	route({
+		responses: {
+			200: {
+				body: "TokenResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const bot = await User.findOneOrFail({ where: { id: req.params.id } });
+		const owner = await User.findOneOrFail({ where: { id: req.user_id } });
 
-	if (owner.id != req.user_id)
-		throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+		if (owner.id != req.user_id)
+			throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
 
-	if (
-		owner.totp_secret &&
-		(!req.body.code || verifyToken(owner.totp_secret, req.body.code))
-	)
-		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+		if (
+			owner.totp_secret &&
+			(!req.body.code || verifyToken(owner.totp_secret, req.body.code))
+		)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
 
-	bot.data = { hash: undefined, valid_tokens_since: new Date() };
+		bot.data = { hash: undefined, valid_tokens_since: new Date() };
 
-	await bot.save();
+		await bot.save();
 
-	const token = await generateToken(bot.id);
+		const token = await generateToken(bot.id);
 
-	res.json({ token }).status(200);
-});
+		res.json({ token }).status(200);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "BotModifySchema" }),
+	route({
+		requestBody: "BotModifySchema",
+		responses: {
+			200: {
+				body: "Application",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as BotModifySchema;
 		if (!body.avatar?.trim()) delete body.avatar;
diff --git a/src/api/routes/applications/#id/entitlements.ts b/src/api/routes/applications/#id/entitlements.ts
index e88fb7f7..6388e6b3 100644
--- a/src/api/routes/applications/#id/entitlements.ts
+++ b/src/api/routes/applications/#id/entitlements.ts
@@ -16,15 +16,25 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), (req: Request, res: Response) => {
-	// TODO:
-	//const { exclude_consumed } = req.query;
-	res.status(200).send([]);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "ApplicationEntitlementsResponse",
+			},
+		},
+	}),
+	(req: Request, res: Response) => {
+		// TODO:
+		//const { exclude_consumed } = req.query;
+		res.status(200).send([]);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/applications/#id/index.ts b/src/api/routes/applications/#id/index.ts
index 067f5dad..c372869a 100644
--- a/src/api/routes/applications/#id/index.ts
+++ b/src/api/routes/applications/#id/index.ts
@@ -16,32 +16,55 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
 import {
 	Application,
-	DiscordApiErrors,
 	ApplicationModifySchema,
+	DiscordApiErrors,
 } from "@spacebar/util";
-import { verifyToken } from "node-2fa";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import { verifyToken } from "node-2fa";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const app = await Application.findOneOrFail({
-		where: { id: req.params.id },
-		relations: ["owner", "bot"],
-	});
-	if (app.owner.id != req.user_id)
-		throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "Application",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const app = await Application.findOneOrFail({
+			where: { id: req.params.id },
+			relations: ["owner", "bot"],
+		});
+		if (app.owner.id != req.user_id)
+			throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
 
-	return res.json(app);
-});
+		return res.json(app);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "ApplicationModifySchema" }),
+	route({
+		requestBody: "ApplicationModifySchema",
+		responses: {
+			200: {
+				body: "Application",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as ApplicationModifySchema;
 
@@ -73,23 +96,35 @@ router.patch(
 	},
 );
 
-router.post("/delete", route({}), async (req: Request, res: Response) => {
-	const app = await Application.findOneOrFail({
-		where: { id: req.params.id },
-		relations: ["bot", "owner"],
-	});
-	if (app.owner.id != req.user_id)
-		throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+router.post(
+	"/delete",
+	route({
+		responses: {
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const app = await Application.findOneOrFail({
+			where: { id: req.params.id },
+			relations: ["bot", "owner"],
+		});
+		if (app.owner.id != req.user_id)
+			throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
 
-	if (
-		app.owner.totp_secret &&
-		(!req.body.code || verifyToken(app.owner.totp_secret, req.body.code))
-	)
-		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+		if (
+			app.owner.totp_secret &&
+			(!req.body.code ||
+				verifyToken(app.owner.totp_secret, req.body.code))
+		)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
 
-	await Application.delete({ id: app.id });
+		await Application.delete({ id: app.id });
 
-	res.send().status(200);
-});
+		res.send().status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/applications/#id/skus.ts b/src/api/routes/applications/#id/skus.ts
index fcb75423..dc4fad23 100644
--- a/src/api/routes/applications/#id/skus.ts
+++ b/src/api/routes/applications/#id/skus.ts
@@ -16,13 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	res.json([]).status(200);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "ApplicationSkusResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		res.json([]).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/applications/detectable.ts b/src/api/routes/applications/detectable.ts
index a8e30894..5cf9d171 100644
--- a/src/api/routes/applications/detectable.ts
+++ b/src/api/routes/applications/detectable.ts
@@ -16,14 +16,24 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	//TODO
-	res.send([]).status(200);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "ApplicationDetectableResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		//TODO
+		res.send([]).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/applications/index.ts b/src/api/routes/applications/index.ts
index 80a19aa8..5bba3338 100644
--- a/src/api/routes/applications/index.ts
+++ b/src/api/routes/applications/index.ts
@@ -16,28 +16,47 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
 import {
 	Application,
 	ApplicationCreateSchema,
-	trimSpecial,
+	Config,
 	User,
+	createAppBotUser,
+	trimSpecial,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const results = await Application.find({
-		where: { owner: { id: req.user_id } },
-		relations: ["owner", "bot"],
-	});
-	res.json(results).status(200);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIApplicationArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const results = await Application.find({
+			where: { owner: { id: req.user_id } },
+			relations: ["owner", "bot"],
+		});
+		res.json(results).status(200);
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "ApplicationCreateSchema" }),
+	route({
+		requestBody: "ApplicationCreateSchema",
+		responses: {
+			200: {
+				body: "Application",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as ApplicationCreateSchema;
 		const user = await User.findOneOrFail({ where: { id: req.user_id } });
@@ -51,7 +70,11 @@ router.post(
 			flags: 0,
 		});
 
-		await app.save();
+		// april 14, 2023: discord made bot users be automatically added to all new apps
+		const { autoCreateBotUsers } = Config.get().general;
+		if (autoCreateBotUsers) {
+			await createAppBotUser(app, req);
+		} else await app.save();
 
 		res.json(app);
 	},
diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts
index e240dff2..6fa86021 100644
--- a/src/api/routes/auth/forgot.ts
+++ b/src/api/routes/auth/forgot.ts
@@ -30,7 +30,18 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "ForgotPasswordSchema" }),
+	route({
+		requestBody: "ForgotPasswordSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorOrCaptchaResponse",
+			},
+			500: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { login, captcha_key } = req.body as ForgotPasswordSchema;
 
diff --git a/src/api/routes/auth/generate-registration-tokens.ts b/src/api/routes/auth/generate-registration-tokens.ts
index 723875f8..80fdaed1 100644
--- a/src/api/routes/auth/generate-registration-tokens.ts
+++ b/src/api/routes/auth/generate-registration-tokens.ts
@@ -16,7 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { route, random } from "@spacebar/api";
+import { random, route } from "@spacebar/api";
 import { Config, ValidRegistrationToken } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 
@@ -25,7 +25,22 @@ export default router;
 
 router.get(
 	"/",
-	route({ right: "OPERATOR" }),
+	route({
+		query: {
+			count: {
+				type: "number",
+				description:
+					"The number of registration tokens to generate. Defaults to 1.",
+			},
+			length: {
+				type: "number",
+				description:
+					"The length of each registration token. Defaults to 255.",
+			},
+		},
+		right: "OPERATOR",
+		responses: { 200: { body: "GenerateRegistrationTokensResponse" } },
+	}),
 	async (req: Request, res: Response) => {
 		const count = req.query.count ? parseInt(req.query.count as string) : 1;
 		const length = req.query.length
diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts
index 52a45c67..28293e59 100644
--- a/src/api/routes/auth/location-metadata.ts
+++ b/src/api/routes/auth/location-metadata.ts
@@ -16,20 +16,29 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { route } from "@spacebar/api";
-import { getIpAdress, IPAnalysis } from "@spacebar/api";
+import { IPAnalysis, getIpAdress, route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	//TODO
-	//Note: It's most likely related to legal. At the moment Discord hasn't finished this too
-	const country_code = (await IPAnalysis(getIpAdress(req))).country_code;
-	res.json({
-		consent_required: false,
-		country_code: country_code,
-		promotional_email_opt_in: { required: true, pre_checked: false },
-	});
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "LocationMetadataResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		//TODO
+		//Note: It's most likely related to legal. At the moment Discord hasn't finished this too
+		const country_code = (await IPAnalysis(getIpAdress(req))).country_code;
+		res.json({
+			consent_required: false,
+			country_code: country_code,
+			promotional_email_opt_in: { required: true, pre_checked: false },
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts
index fe0b4f99..d3fc1fb4 100644
--- a/src/api/routes/auth/login.ts
+++ b/src/api/routes/auth/login.ts
@@ -36,7 +36,17 @@ export default router;
 
 router.post(
 	"/",
-	route({ body: "LoginSchema" }),
+	route({
+		requestBody: "LoginSchema",
+		responses: {
+			200: {
+				body: "LoginResponse",
+			},
+			400: {
+				body: "APIErrorOrCaptchaResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { login, password, captcha_key, undelete } =
 			req.body as LoginSchema;
diff --git a/src/api/routes/auth/logout.ts b/src/api/routes/auth/logout.ts
index 51909afa..94a3e474 100644
--- a/src/api/routes/auth/logout.ts
+++ b/src/api/routes/auth/logout.ts
@@ -22,14 +22,25 @@ import { Request, Response, Router } from "express";
 const router: Router = Router();
 export default router;
 
-router.post("/", route({}), async (req: Request, res: Response) => {
-	if (req.body.provider != null || req.body.voip_provider != null) {
-		console.log(`[LOGOUT]: provider or voip provider not null!`, req.body);
-	} else {
-		delete req.body.provider;
-		delete req.body.voip_provider;
-		if (Object.keys(req.body).length != 0)
-			console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body);
-	}
-	res.status(204).send();
-});
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		if (req.body.provider != null || req.body.voip_provider != null) {
+			console.log(
+				`[LOGOUT]: provider or voip provider not null!`,
+				req.body,
+			);
+		} else {
+			delete req.body.provider;
+			delete req.body.voip_provider;
+			if (Object.keys(req.body).length != 0)
+				console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body);
+		}
+		res.status(204).send();
+	},
+);
diff --git a/src/api/routes/auth/mfa/totp.ts b/src/api/routes/auth/mfa/totp.ts
index 2396443d..4df408f9 100644
--- a/src/api/routes/auth/mfa/totp.ts
+++ b/src/api/routes/auth/mfa/totp.ts
@@ -16,16 +16,26 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
-import { BackupCode, generateToken, User, TotpSchema } from "@spacebar/util";
-import { verifyToken } from "node-2fa";
+import { BackupCode, TotpSchema, User, generateToken } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import { verifyToken } from "node-2fa";
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "TotpSchema" }),
+	route({
+		requestBody: "TotpSchema",
+		responses: {
+			200: {
+				body: "TokenResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		// const { code, ticket, gift_code_sku_id, login_source } =
 		const { code, ticket } = req.body as TotpSchema;
diff --git a/src/api/routes/auth/mfa/webauthn.ts b/src/api/routes/auth/mfa/webauthn.ts
index 1b387411..b58d2944 100644
--- a/src/api/routes/auth/mfa/webauthn.ts
+++ b/src/api/routes/auth/mfa/webauthn.ts
@@ -41,7 +41,13 @@ function toArrayBuffer(buf: Buffer) {
 
 router.post(
 	"/",
-	route({ body: "WebAuthnTotpSchema" }),
+	route({
+		requestBody: "WebAuthnTotpSchema",
+		responses: {
+			200: { body: "TokenResponse" },
+			400: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		if (!WebAuthn.fido2) {
 			// TODO: I did this for typescript and I can't use !
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index 430c9532..14dc319a 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -16,25 +16,25 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import {
+	IPAnalysis,
+	getIpAdress,
+	isProxy,
+	route,
+	verifyCaptcha,
+} from "@spacebar/api";
 import {
 	Config,
-	generateToken,
-	Invite,
 	FieldErrors,
-	User,
-	adjustEmail,
+	Invite,
 	RegisterSchema,
+	User,
 	ValidRegistrationToken,
+	adjustEmail,
+	generateToken,
 } from "@spacebar/util";
-import {
-	route,
-	getIpAdress,
-	IPAnalysis,
-	isProxy,
-	verifyCaptcha,
-} from "@spacebar/api";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { MoreThan } from "typeorm";
 
@@ -42,7 +42,13 @@ const router: Router = Router();
 
 router.post(
 	"/",
-	route({ body: "RegisterSchema" }),
+	route({
+		requestBody: "RegisterSchema",
+		responses: {
+			200: { body: "TokenOnlyResponse" },
+			400: { body: "APIErrorOrCaptchaResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as RegisterSchema;
 		const { register, security, limits } = Config.get();
@@ -219,6 +225,20 @@ router.post(
 		}
 
 		if (body.password) {
+			const min = register.password.minLength
+				? register.password.minLength
+				: 8;
+			if (body.password.length < min) {
+				throw FieldErrors({
+					password: {
+						code: "PASSWORD_REQUIREMENTS_MIN_LENGTH",
+						message: req.t(
+							"auth:register.PASSWORD_REQUIREMENTS_MIN_LENGTH",
+							{ min: min },
+						),
+					},
+				});
+			}
 			// the salt is saved in the password refer to bcrypt docs
 			body.password = await bcrypt.hash(body.password, 12);
 		} else if (register.password.required) {
diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts
index 852a43c7..b3ca1e9e 100644
--- a/src/api/routes/auth/reset.ts
+++ b/src/api/routes/auth/reset.ts
@@ -19,7 +19,6 @@
 import { route } from "@spacebar/api";
 import {
 	checkToken,
-	Config,
 	Email,
 	FieldErrors,
 	generateToken,
@@ -31,17 +30,26 @@ import { Request, Response, Router } from "express";
 
 const router = Router();
 
+// TODO: the response interface also returns settings, but this route doesn't actually return that.
 router.post(
 	"/",
-	route({ body: "PasswordResetSchema" }),
+	route({
+		requestBody: "PasswordResetSchema",
+		responses: {
+			200: {
+				body: "TokenOnlyResponse",
+			},
+			400: {
+				body: "APIErrorOrCaptchaResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { password, token } = req.body as PasswordResetSchema;
 
-		const { jwtSecret } = Config.get().security;
-
 		let user;
 		try {
-			const userTokenData = await checkToken(token, jwtSecret, true);
+			const userTokenData = await checkToken(token);
 			user = userTokenData.user;
 		} catch {
 			throw FieldErrors({
diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts
index c1afcde9..49f74277 100644
--- a/src/api/routes/auth/verify/index.ts
+++ b/src/api/routes/auth/verify/index.ts
@@ -37,9 +37,20 @@ async function getToken(user: User) {
 	return { token };
 }
 
+// TODO: the response interface also returns settings, but this route doesn't actually return that.
 router.post(
 	"/",
-	route({ body: "VerifyEmailSchema" }),
+	route({
+		requestBody: "VerifyEmailSchema",
+		responses: {
+			200: {
+				body: "TokenResponse",
+			},
+			400: {
+				body: "APIErrorOrCaptchaResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { captcha_key, token } = req.body;
 
@@ -67,11 +78,10 @@ router.post(
 			}
 		}
 
-		const { jwtSecret } = Config.get().security;
 		let user;
 
 		try {
-			const userTokenData = await checkToken(token, jwtSecret, true);
+			const userTokenData = await checkToken(token);
 			user = userTokenData.user;
 		} catch {
 			throw FieldErrors({
diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts
index f2727abd..701f0ea8 100644
--- a/src/api/routes/auth/verify/resend.ts
+++ b/src/api/routes/auth/verify/resend.ts
@@ -24,7 +24,18 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ right: "RESEND_VERIFICATION_EMAIL" }),
+	route({
+		right: "RESEND_VERIFICATION_EMAIL",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			500: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const user = await User.findOneOrFail({
 			where: { id: req.user_id },
diff --git a/src/api/routes/auth/verify/view-backup-codes-challenge.ts b/src/api/routes/auth/verify/view-backup-codes-challenge.ts
index b12719ff..5407de82 100644
--- a/src/api/routes/auth/verify/view-backup-codes-challenge.ts
+++ b/src/api/routes/auth/verify/view-backup-codes-challenge.ts
@@ -16,15 +16,21 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
-import { FieldErrors, User, BackupCodesChallengeSchema } from "@spacebar/util";
+import { BackupCodesChallengeSchema, FieldErrors, User } from "@spacebar/util";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "BackupCodesChallengeSchema" }),
+	route({
+		requestBody: "BackupCodesChallengeSchema",
+		responses: {
+			200: { body: "BackupCodesChallengeResponse" },
+			400: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { password } = req.body as BackupCodesChallengeSchema;
 
diff --git a/src/api/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts
index db0d4242..567c7c92 100644
--- a/src/api/routes/channels/#channel_id/index.ts
+++ b/src/api/routes/channels/#channel_id/index.ts
@@ -16,18 +16,18 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelDeleteEvent,
+	ChannelModifySchema,
 	ChannelType,
 	ChannelUpdateEvent,
-	emitEvent,
 	Recipient,
+	emitEvent,
 	handleFile,
-	ChannelModifySchema,
 } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { route } from "@spacebar/api";
 
 const router: Router = Router();
 // TODO: delete channel
@@ -35,7 +35,15 @@ const router: Router = Router();
 
 router.get(
 	"/",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			200: {
+				body: "Channel",
+			},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 
@@ -49,7 +57,15 @@ router.get(
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_CHANNELS" }),
+	route({
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			200: {
+				body: "Channel",
+			},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 
@@ -90,7 +106,19 @@ router.delete(
 
 router.patch(
 	"/",
-	route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }),
+	route({
+		requestBody: "ChannelModifySchema",
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			200: {
+				body: "Channel",
+			},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const payload = req.body as ChannelModifySchema;
 		const { channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts
index 9f247fe8..b02f65d3 100644
--- a/src/api/routes/channels/#channel_id/invites.ts
+++ b/src/api/routes/channels/#channel_id/invites.ts
@@ -16,29 +16,37 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
-import { random } from "@spacebar/api";
+import { random, route } from "@spacebar/api";
 import {
 	Channel,
+	Guild,
 	Invite,
 	InviteCreateEvent,
-	emitEvent,
-	User,
-	Guild,
 	PublicInviteRelation,
+	User,
+	emitEvent,
+	isTextChannel,
 } from "@spacebar/util";
-import { isTextChannel } from "./messages";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
 
 const router: Router = Router();
 
 router.post(
 	"/",
 	route({
-		body: "InviteCreateSchema",
+		requestBody: "InviteCreateSchema",
 		permission: "CREATE_INSTANT_INVITE",
 		right: "CREATE_INVITES",
+		responses: {
+			201: {
+				body: "Invite",
+			},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { user_id } = req;
@@ -84,7 +92,15 @@ router.post(
 
 router.get(
 	"/",
-	route({ permission: "MANAGE_CHANNELS" }),
+	route({
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			200: {
+				body: "APIInviteArray",
+			},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 		const channel = await Channel.findOneOrFail({
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
index f098fa8e..a6dcae6b 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	emitEvent,
 	getPermission,
@@ -23,7 +24,6 @@ import {
 	ReadState,
 } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { route } from "@spacebar/api";
 
 const router = Router();
 
@@ -33,7 +33,13 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "MessageAcknowledgeSchema" }),
+	route({
+		requestBody: "MessageAcknowledgeSchema",
+		responses: {
+			200: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
 
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
index 909a459e..5ca645c0 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
@@ -16,14 +16,21 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.post(
 	"/",
-	route({ permission: "MANAGE_MESSAGES" }),
+	route({
+		permission: "MANAGE_MESSAGES",
+		responses: {
+			200: {
+				body: "Message",
+			},
+		},
+	}),
 	(req: Request, res: Response) => {
 		// TODO:
 		res.json({
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
index cd4b243e..6bc03f53 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -19,24 +19,23 @@
 import {
 	Attachment,
 	Channel,
-	emitEvent,
-	SpacebarApiErrors,
-	getPermission,
-	getRights,
 	Message,
 	MessageCreateEvent,
+	MessageCreateSchema,
 	MessageDeleteEvent,
+	MessageEditSchema,
 	MessageUpdateEvent,
 	Snowflake,
+	SpacebarApiErrors,
+	emitEvent,
+	getPermission,
+	getRights,
 	uploadFile,
-	MessageCreateSchema,
-	MessageEditSchema,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
-import multer from "multer";
-import { route } from "@spacebar/api";
-import { handleMessage, postHandleMessage } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import multer from "multer";
+import { handleMessage, postHandleMessage, route } from "../../../../../util";
 
 const router = Router();
 // TODO: message content/embed string length limit
@@ -53,9 +52,19 @@ const messageUpload = multer({
 router.patch(
 	"/",
 	route({
-		body: "MessageEditSchema",
+		requestBody: "MessageEditSchema",
 		permission: "SEND_MESSAGES",
 		right: "SEND_MESSAGES",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
@@ -143,9 +152,19 @@ router.put(
 		next();
 	},
 	route({
-		body: "MessageCreateSchema",
+		requestBody: "MessageCreateSchema",
 		permission: "SEND_MESSAGES",
 		right: "SEND_BACKDATED_EVENTS",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
@@ -230,7 +249,19 @@ router.put(
 
 router.get(
 	"/",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 
@@ -252,38 +283,54 @@ router.get(
 	},
 );
 
-router.delete("/", route({}), async (req: Request, res: Response) => {
-	const { message_id, channel_id } = req.params;
+router.delete(
+	"/",
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { message_id, channel_id } = req.params;
 
-	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
-	const message = await Message.findOneOrFail({ where: { id: message_id } });
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+		});
+		const message = await Message.findOneOrFail({
+			where: { id: message_id },
+		});
 
-	const rights = await getRights(req.user_id);
+		const rights = await getRights(req.user_id);
 
-	if (message.author_id !== req.user_id) {
-		if (!rights.has("MANAGE_MESSAGES")) {
-			const permission = await getPermission(
-				req.user_id,
-				channel.guild_id,
-				channel_id,
-			);
-			permission.hasThrow("MANAGE_MESSAGES");
-		}
-	} else rights.hasThrow("SELF_DELETE_MESSAGES");
+		if (message.author_id !== req.user_id) {
+			if (!rights.has("MANAGE_MESSAGES")) {
+				const permission = await getPermission(
+					req.user_id,
+					channel.guild_id,
+					channel_id,
+				);
+				permission.hasThrow("MANAGE_MESSAGES");
+			}
+		} else rights.hasThrow("SELF_DELETE_MESSAGES");
 
-	await Message.delete({ id: message_id });
+		await Message.delete({ id: message_id });
 
-	await emitEvent({
-		event: "MESSAGE_DELETE",
-		channel_id,
-		data: {
-			id: message_id,
+		await emitEvent({
+			event: "MESSAGE_DELETE",
 			channel_id,
-			guild_id: channel.guild_id,
-		},
-	} as MessageDeleteEvent);
+			data: {
+				id: message_id,
+				channel_id,
+				guild_id: channel.guild_id,
+			},
+		} as MessageDeleteEvent);
 
-	res.sendStatus(204);
-});
+		res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
index cb66cd64..5efa0f14 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	emitEvent,
@@ -32,8 +33,7 @@ import {
 	PublicUserProjection,
 	User,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { In } from "typeorm";
 
@@ -57,7 +57,17 @@ function getEmoji(emoji: string): PartialEmoji {
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_MESSAGES" }),
+	route({
+		permission: "MANAGE_MESSAGES",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 
@@ -83,7 +93,17 @@ router.delete(
 
 router.delete(
 	"/:emoji",
-	route({ permission: "MANAGE_MESSAGES" }),
+	route({
+		permission: "MANAGE_MESSAGES",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 		const emoji = getEmoji(req.params.emoji);
@@ -120,7 +140,19 @@ router.delete(
 
 router.get(
 	"/:emoji",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			200: {
+				body: "PublicUser",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 		const emoji = getEmoji(req.params.emoji);
@@ -148,7 +180,18 @@ router.get(
 
 router.put(
 	"/:emoji/:user_id",
-	route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }),
+	route({
+		permission: "READ_MESSAGE_HISTORY",
+		right: "SELF_ADD_REACTIONS",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id, user_id } = req.params;
 		if (user_id !== "@me") throw new HTTPError("Invalid user");
@@ -219,7 +262,16 @@ router.put(
 
 router.delete(
 	"/:emoji/:user_id",
-	route({}),
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		let { user_id } = req.params;
 		const { message_id, channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
index 18476d5c..9b607d59 100644
--- a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
+++ b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -16,18 +16,18 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	Config,
 	emitEvent,
 	getPermission,
 	getRights,
-	MessageDeleteBulkEvent,
 	Message,
+	MessageDeleteBulkEvent,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 
 const router: Router = Router();
 
@@ -38,7 +38,17 @@ export default router;
 // https://discord.com/developers/docs/resources/channel#bulk-delete-messages
 router.post(
 	"/",
-	route({ body: "BulkDeleteSchema" }),
+	route({
+		requestBody: "BulkDeleteSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 		const channel = await Channel.findOneOrFail({
diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts
index 7f0c9fb5..c384a05b 100644
--- a/src/api/routes/channels/#channel_id/messages/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -16,128 +16,137 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { handleMessage, postHandleMessage, route } from "@spacebar/api";
 import {
 	Attachment,
 	Channel,
-	ChannelType,
 	Config,
 	DmChannelDTO,
-	emitEvent,
 	FieldErrors,
-	getPermission,
+	Member,
 	Message,
 	MessageCreateEvent,
-	Snowflake,
-	uploadFile,
-	Member,
 	MessageCreateSchema,
+	Reaction,
 	ReadState,
 	Rights,
-	Reaction,
+	Snowflake,
 	User,
+	emitEvent,
+	getPermission,
+	isTextChannel,
+	uploadFile,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { handleMessage, postHandleMessage, route } from "@spacebar/api";
 import multer from "multer";
 import { FindManyOptions, FindOperator, LessThan, MoreThan } from "typeorm";
 import { URL } from "url";
 
 const router: Router = Router();
 
-export default router;
-
-export function isTextChannel(type: ChannelType): boolean {
-	switch (type) {
-		case ChannelType.GUILD_STORE:
-		case ChannelType.GUILD_VOICE:
-		case ChannelType.GUILD_STAGE_VOICE:
-		case ChannelType.GUILD_CATEGORY:
-		case ChannelType.GUILD_FORUM:
-		case ChannelType.DIRECTORY:
-			throw new HTTPError("not a text channel", 400);
-		case ChannelType.DM:
-		case ChannelType.GROUP_DM:
-		case ChannelType.GUILD_NEWS:
-		case ChannelType.GUILD_NEWS_THREAD:
-		case ChannelType.GUILD_PUBLIC_THREAD:
-		case ChannelType.GUILD_PRIVATE_THREAD:
-		case ChannelType.GUILD_TEXT:
-		case ChannelType.ENCRYPTED:
-		case ChannelType.ENCRYPTED_THREAD:
-			return true;
-		default:
-			throw new HTTPError("unimplemented", 400);
-	}
-}
-
 // https://discord.com/developers/docs/resources/channel#create-message
 // get messages
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const channel_id = req.params.channel_id;
-	const channel = await Channel.findOneOrFail({
-		where: { id: channel_id },
-	});
-	if (!channel) throw new HTTPError("Channel not found", 404);
-
-	isTextChannel(channel.type);
-	const around = req.query.around ? `${req.query.around}` : undefined;
-	const before = req.query.before ? `${req.query.before}` : undefined;
-	const after = req.query.after ? `${req.query.after}` : undefined;
-	const limit = Number(req.query.limit) || 50;
-	if (limit < 1 || limit > 100)
-		throw new HTTPError("limit must be between 1 and 100", 422);
-
-	const halfLimit = Math.floor(limit / 2);
-
-	const permissions = await getPermission(
-		req.user_id,
-		channel.guild_id,
-		channel_id,
-	);
-	permissions.hasThrow("VIEW_CHANNEL");
-	if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
-
-	const query: FindManyOptions<Message> & {
-		where: { id?: FindOperator<string> | FindOperator<string>[] };
-	} = {
-		order: { timestamp: "DESC" },
-		take: limit,
-		where: { channel_id },
-		relations: [
-			"author",
-			"webhook",
-			"application",
-			"mentions",
-			"mention_roles",
-			"mention_channels",
-			"sticker_items",
-			"attachments",
-		],
-	};
-
-	if (after) {
-		if (BigInt(after) > BigInt(Snowflake.generate()))
-			return res.status(422);
-		query.where.id = MoreThan(after);
-	} else if (before) {
-		if (BigInt(before) < BigInt(req.params.channel_id))
-			return res.status(422);
-		query.where.id = LessThan(before);
-	} else if (around) {
-		query.where.id = [
-			MoreThan((BigInt(around) - BigInt(halfLimit)).toString()),
-			LessThan((BigInt(around) + BigInt(halfLimit)).toString()),
-		];
-
-		return res.json([]); // TODO: fix around
-	}
-
-	const messages = await Message.find(query);
-	const endpoint = Config.get().cdn.endpointPublic;
-
-	return res.json(
-		messages.map((x: Partial<Message>) => {
+router.get(
+	"/",
+	route({
+		query: {
+			around: {
+				type: "string",
+			},
+			before: {
+				type: "string",
+			},
+			after: {
+				type: "string",
+			},
+			limit: {
+				type: "number",
+				description:
+					"max number of messages to return (1-100). defaults to 50",
+			},
+		},
+		responses: {
+			200: {
+				body: "APIMessageArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const channel_id = req.params.channel_id;
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+		});
+		if (!channel) throw new HTTPError("Channel not found", 404);
+
+		isTextChannel(channel.type);
+		const around = req.query.around ? `${req.query.around}` : undefined;
+		const before = req.query.before ? `${req.query.before}` : undefined;
+		const after = req.query.after ? `${req.query.after}` : undefined;
+		const limit = Number(req.query.limit) || 50;
+		if (limit < 1 || limit > 100)
+			throw new HTTPError("limit must be between 1 and 100", 422);
+
+		const permissions = await getPermission(
+			req.user_id,
+			channel.guild_id,
+			channel_id,
+		);
+		permissions.hasThrow("VIEW_CHANNEL");
+		if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
+
+		const query: FindManyOptions<Message> & {
+			where: { id?: FindOperator<string> | FindOperator<string>[] };
+		} = {
+			order: { timestamp: "DESC" },
+			take: limit,
+			where: { channel_id },
+			relations: [
+				"author",
+				"webhook",
+				"application",
+				"mentions",
+				"mention_roles",
+				"mention_channels",
+				"sticker_items",
+				"attachments",
+			],
+		};
+
+		let messages: Message[];
+
+		if (around) {
+			query.take = Math.floor(limit / 2);
+			const [right, left] = await Promise.all([
+				Message.find({ ...query, where: { id: LessThan(around) } }),
+				Message.find({ ...query, where: { id: MoreThan(around) } }),
+			]);
+			right.push(...left);
+			messages = right;
+		} else {
+			if (after) {
+				if (BigInt(after) > BigInt(Snowflake.generate()))
+					return res.status(422);
+				query.where.id = MoreThan(after);
+			} else if (before) {
+				if (BigInt(before) > BigInt(Snowflake.generate()))
+					return res.status(422);
+				query.where.id = LessThan(before);
+			}
+
+			messages = await Message.find(query);
+		}
+
+		const endpoint = Config.get().cdn.endpointPublic;
+
+		const ret = messages.map((x: Message) => {
+			x = x.toJSON();
+
 			(x.reactions || []).forEach((y: Partial<Reaction>) => {
 				// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 				//@ts-ignore
@@ -172,9 +181,11 @@ router.get("/", route({}), async (req: Request, res: Response) => {
 			// }
 
 			return x;
-		}),
-	);
-});
+		});
+
+		return res.json(ret);
+	},
+);
 
 // TODO: config max upload size
 const messageUpload = multer({
@@ -205,9 +216,19 @@ router.post(
 		next();
 	},
 	route({
-		body: "MessageCreateSchema",
+		requestBody: "MessageCreateSchema",
 		permission: "SEND_MESSAGES",
 		right: "SEND_MESSAGES",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
@@ -288,9 +309,11 @@ router.post(
 			embeds,
 			channel_id,
 			attachments,
-			edited_timestamp: undefined,
 			timestamp: new Date(),
 		});
+		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+		//@ts-ignore dont care2
+		message.edited_timestamp = null;
 
 		channel.last_message_id = message.id;
 
@@ -366,3 +389,5 @@ router.post(
 		return res.json(message);
 	},
 );
+
+export default router;
diff --git a/src/api/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts
index 68dbc2f2..d3edb0fa 100644
--- a/src/api/routes/channels/#channel_id/permissions.ts
+++ b/src/api/routes/channels/#channel_id/permissions.ts
@@ -19,13 +19,13 @@
 import {
 	Channel,
 	ChannelPermissionOverwrite,
+	ChannelPermissionOverwriteSchema,
 	ChannelUpdateEvent,
 	emitEvent,
 	Member,
 	Role,
-	ChannelPermissionOverwriteSchema,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 import { route } from "@spacebar/api";
@@ -36,8 +36,14 @@ const router: Router = Router();
 router.put(
 	"/:overwrite_id",
 	route({
-		body: "ChannelPermissionOverwriteSchema",
+		requestBody: "ChannelPermissionOverwriteSchema",
 		permission: "MANAGE_ROLES",
+		responses: {
+			204: {},
+			404: {},
+			501: {},
+			400: { body: "APIErrorResponse" },
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, overwrite_id } = req.params;
@@ -92,7 +98,7 @@ router.put(
 // TODO: check permission hierarchy
 router.delete(
 	"/:overwrite_id",
-	route({ permission: "MANAGE_ROLES" }),
+	route({ permission: "MANAGE_ROLES", responses: { 204: {}, 404: {} } }),
 	async (req: Request, res: Response) => {
 		const { channel_id, overwrite_id } = req.params;
 
diff --git a/src/api/routes/channels/#channel_id/pins.ts b/src/api/routes/channels/#channel_id/pins.ts
index 32820916..724ebffd 100644
--- a/src/api/routes/channels/#channel_id/pins.ts
+++ b/src/api/routes/channels/#channel_id/pins.ts
@@ -16,23 +16,33 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelPinsUpdateEvent,
 	Config,
+	DiscordApiErrors,
 	emitEvent,
 	Message,
 	MessageUpdateEvent,
-	DiscordApiErrors,
 } from "@spacebar/util";
-import { Router, Request, Response } from "express";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.put(
 	"/:message_id",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			204: {},
+			403: {},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
 
@@ -74,7 +84,17 @@ router.put(
 
 router.delete(
 	"/:message_id",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			204: {},
+			403: {},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
 
@@ -114,7 +134,17 @@ router.delete(
 
 router.get(
 	"/",
-	route({ permission: ["READ_MESSAGE_HISTORY"] }),
+	route({
+		permission: ["READ_MESSAGE_HISTORY"],
+		responses: {
+			200: {
+				body: "APIMessageArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 
diff --git a/src/api/routes/channels/#channel_id/purge.ts b/src/api/routes/channels/#channel_id/purge.ts
index c8da6760..012fec1c 100644
--- a/src/api/routes/channels/#channel_id/purge.ts
+++ b/src/api/routes/channels/#channel_id/purge.ts
@@ -16,20 +16,20 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { HTTPError } from "lambert-server";
 import { route } from "@spacebar/api";
-import { isTextChannel } from "./messages";
-import { FindManyOptions, Between, Not, FindOperator } from "typeorm";
 import {
 	Channel,
-	emitEvent,
-	getPermission,
-	getRights,
 	Message,
 	MessageDeleteBulkEvent,
 	PurgeSchema,
+	emitEvent,
+	getPermission,
+	getRights,
+	isTextChannel,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+import { Between, FindManyOptions, FindOperator, Not } from "typeorm";
 
 const router: Router = Router();
 
@@ -42,6 +42,14 @@ router.post(
 	"/",
 	route({
 		/*body: "PurgeSchema",*/
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts
index f1fb48af..569bb5cd 100644
--- a/src/api/routes/channels/#channel_id/recipients.ts
+++ b/src/api/routes/channels/#channel_id/recipients.ts
@@ -16,7 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelRecipientAddEvent,
@@ -28,80 +28,98 @@ import {
 	Recipient,
 	User,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.put("/:user_id", route({}), async (req: Request, res: Response) => {
-	const { channel_id, user_id } = req.params;
-	const channel = await Channel.findOneOrFail({
-		where: { id: channel_id },
-		relations: ["recipients"],
-	});
+router.put(
+	"/:user_id",
+	route({
+		responses: {
+			201: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { channel_id, user_id } = req.params;
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+			relations: ["recipients"],
+		});
 
-	if (channel.type !== ChannelType.GROUP_DM) {
-		const recipients = [
-			...(channel.recipients?.map((r) => r.user_id) || []),
-			user_id,
-		].unique();
+		if (channel.type !== ChannelType.GROUP_DM) {
+			const recipients = [
+				...(channel.recipients?.map((r) => r.user_id) || []),
+				user_id,
+			].unique();
 
-		const new_channel = await Channel.createDMChannel(
-			recipients,
-			req.user_id,
-		);
-		return res.status(201).json(new_channel);
-	} else {
-		if (channel.recipients?.map((r) => r.user_id).includes(user_id)) {
-			throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
-		}
+			const new_channel = await Channel.createDMChannel(
+				recipients,
+				req.user_id,
+			);
+			return res.status(201).json(new_channel);
+		} else {
+			if (channel.recipients?.map((r) => r.user_id).includes(user_id)) {
+				throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
+			}
 
-		channel.recipients?.push(
-			Recipient.create({ channel_id: channel_id, user_id: user_id }),
-		);
-		await channel.save();
+			channel.recipients?.push(
+				Recipient.create({ channel_id: channel_id, user_id: user_id }),
+			);
+			await channel.save();
 
-		await emitEvent({
-			event: "CHANNEL_CREATE",
-			data: await DmChannelDTO.from(channel, [user_id]),
-			user_id: user_id,
-		});
+			await emitEvent({
+				event: "CHANNEL_CREATE",
+				data: await DmChannelDTO.from(channel, [user_id]),
+				user_id: user_id,
+			});
 
-		await emitEvent({
-			event: "CHANNEL_RECIPIENT_ADD",
-			data: {
+			await emitEvent({
+				event: "CHANNEL_RECIPIENT_ADD",
+				data: {
+					channel_id: channel_id,
+					user: await User.findOneOrFail({
+						where: { id: user_id },
+						select: PublicUserProjection,
+					}),
+				},
 				channel_id: channel_id,
-				user: await User.findOneOrFail({
-					where: { id: user_id },
-					select: PublicUserProjection,
-				}),
-			},
-			channel_id: channel_id,
-		} as ChannelRecipientAddEvent);
-		return res.sendStatus(204);
-	}
-});
+			} as ChannelRecipientAddEvent);
+			return res.sendStatus(204);
+		}
+	},
+);
 
-router.delete("/:user_id", route({}), async (req: Request, res: Response) => {
-	const { channel_id, user_id } = req.params;
-	const channel = await Channel.findOneOrFail({
-		where: { id: channel_id },
-		relations: ["recipients"],
-	});
-	if (
-		!(
-			channel.type === ChannelType.GROUP_DM &&
-			(channel.owner_id === req.user_id || user_id === req.user_id)
+router.delete(
+	"/:user_id",
+	route({
+		responses: {
+			204: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { channel_id, user_id } = req.params;
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+			relations: ["recipients"],
+		});
+		if (
+			!(
+				channel.type === ChannelType.GROUP_DM &&
+				(channel.owner_id === req.user_id || user_id === req.user_id)
+			)
 		)
-	)
-		throw DiscordApiErrors.MISSING_PERMISSIONS;
+			throw DiscordApiErrors.MISSING_PERMISSIONS;
 
-	if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) {
-		throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
-	}
+		if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) {
+			throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
+		}
 
-	await Channel.removeRecipientFromChannel(channel, user_id);
+		await Channel.removeRecipientFromChannel(channel, user_id);
 
-	return res.sendStatus(204);
-});
+		return res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/channels/#channel_id/typing.ts b/src/api/routes/channels/#channel_id/typing.ts
index 6a2fef39..b5d61d74 100644
--- a/src/api/routes/channels/#channel_id/typing.ts
+++ b/src/api/routes/channels/#channel_id/typing.ts
@@ -16,15 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util";
 import { route } from "@spacebar/api";
-import { Router, Request, Response } from "express";
+import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.post(
 	"/",
-	route({ permission: "SEND_MESSAGES" }),
+	route({
+		permission: "SEND_MESSAGES",
+		responses: {
+			204: {},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 		const user_id = req.user_id;
diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts
index 14791a1c..d54756a1 100644
--- a/src/api/routes/channels/#channel_id/webhooks.ts
+++ b/src/api/routes/channels/#channel_id/webhooks.ts
@@ -16,34 +16,56 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
 import {
 	Channel,
 	Config,
-	handleFile,
-	trimSpecial,
+	DiscordApiErrors,
 	User,
 	Webhook,
 	WebhookCreateSchema,
 	WebhookType,
+	handleFile,
+	trimSpecial,
+	isTextChannel,
 } from "@spacebar/util";
-import { HTTPError } from "lambert-server";
-import { isTextChannel } from "./messages/index";
-import { DiscordApiErrors } from "@spacebar/util";
 import crypto from "crypto";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
 
 const router: Router = Router();
 
 //TODO: implement webhooks
-router.get("/", route({}), async (req: Request, res: Response) => {
-	res.json([]);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIWebhookArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		res.json([]);
+	},
+);
 
 // TODO: use Image Data Type for avatar instead of String
 router.post(
 	"/",
-	route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }),
+	route({
+		requestBody: "WebhookCreateSchema",
+		permission: "MANAGE_WEBHOOKS",
+		responses: {
+			200: {
+				body: "WebhookCreateResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const channel_id = req.params.channel_id;
 		const channel = await Channel.findOneOrFail({
diff --git a/src/api/routes/connections/#connection_name/#connection_id/refresh.ts b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts
index 0d432c2b..d44cf314 100644
--- a/src/api/routes/connections/#connection_name/#connection_id/refresh.ts
+++ b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts
@@ -22,7 +22,7 @@ const router = Router();
 
 router.post("/", route({}), async (req: Request, res: Response) => {
 	// TODO:
-	const { connection_name, connection_id } = req.params;
+	// const { connection_name, connection_id } = req.params;
 	res.sendStatus(204);
 });
 
diff --git a/src/api/routes/connections/#connection_name/callback.ts b/src/api/routes/connections/#connection_name/callback.ts
index bc9ba455..ee0db94a 100644
--- a/src/api/routes/connections/#connection_name/callback.ts
+++ b/src/api/routes/connections/#connection_name/callback.ts
@@ -29,7 +29,7 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "ConnectionCallbackSchema" }),
+	route({ requestBody: "ConnectionCallbackSchema" }),
 	async (req: Request, res: Response) => {
 		const { connection_name } = req.params;
 		const connection = ConnectionStore.connections.get(connection_name);
diff --git a/src/api/routes/discoverable-guilds.ts b/src/api/routes/discoverable-guilds.ts
index 75eb6088..b8c6a386 100644
--- a/src/api/routes/discoverable-guilds.ts
+++ b/src/api/routes/discoverable-guilds.ts
@@ -16,49 +16,61 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Guild, Config } from "@spacebar/util";
+import { Config, Guild } from "@spacebar/util";
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { Like } from "typeorm";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { offset, limit, categories } = req.query;
-	const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
-	const configLimit = Config.get().guild.discovery.limit;
-	let guilds;
-	if (categories == undefined) {
-		guilds = showAllGuilds
-			? await Guild.find({ take: Math.abs(Number(limit || configLimit)) })
-			: await Guild.find({
-					where: { features: Like(`%DISCOVERABLE%`) },
-					take: Math.abs(Number(limit || configLimit)),
-			  });
-	} else {
-		guilds = showAllGuilds
-			? await Guild.find({
-					where: { primary_category_id: categories.toString() },
-					take: Math.abs(Number(limit || configLimit)),
-			  })
-			: await Guild.find({
-					where: {
-						primary_category_id: categories.toString(),
-						features: Like("%DISCOVERABLE%"),
-					},
-					take: Math.abs(Number(limit || configLimit)),
-			  });
-	}
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "DiscoverableGuildsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { offset, limit, categories } = req.query;
+		const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
+		const configLimit = Config.get().guild.discovery.limit;
+		let guilds;
+		if (categories == undefined) {
+			guilds = showAllGuilds
+				? await Guild.find({
+						take: Math.abs(Number(limit || configLimit)),
+				  })
+				: await Guild.find({
+						where: { features: Like(`%DISCOVERABLE%`) },
+						take: Math.abs(Number(limit || configLimit)),
+				  });
+		} else {
+			guilds = showAllGuilds
+				? await Guild.find({
+						where: { primary_category_id: categories.toString() },
+						take: Math.abs(Number(limit || configLimit)),
+				  })
+				: await Guild.find({
+						where: {
+							primary_category_id: categories.toString(),
+							features: Like("%DISCOVERABLE%"),
+						},
+						take: Math.abs(Number(limit || configLimit)),
+				  });
+		}
 
-	const total = guilds ? guilds.length : undefined;
+		const total = guilds ? guilds.length : undefined;
 
-	res.send({
-		total: total,
-		guilds: guilds,
-		offset: Number(offset || Config.get().guild.discovery.offset),
-		limit: Number(limit || configLimit),
-	});
-});
+		res.send({
+			total: total,
+			guilds: guilds,
+			offset: Number(offset || Config.get().guild.discovery.offset),
+			limit: Number(limit || configLimit),
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/discovery.ts b/src/api/routes/discovery.ts
index 0c8089e4..a045c191 100644
--- a/src/api/routes/discovery.ts
+++ b/src/api/routes/discovery.ts
@@ -16,24 +16,34 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Categories } from "@spacebar/util";
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { Categories } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/categories", route({}), async (req: Request, res: Response) => {
-	// TODO:
-	// Get locale instead
+router.get(
+	"/categories",
+	route({
+		responses: {
+			200: {
+				body: "APIDiscoveryCategoryArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO:
+		// Get locale instead
 
-	// const { locale, primary_only } = req.query;
-	const { primary_only } = req.query;
+		// const { locale, primary_only } = req.query;
+		const { primary_only } = req.query;
 
-	const out = primary_only
-		? await Categories.find()
-		: await Categories.find({ where: { is_primary: true } });
+		const out = primary_only
+			? await Categories.find()
+			: await Categories.find({ where: { is_primary: true } });
 
-	res.send(out);
-});
+		res.send(out);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/download.ts b/src/api/routes/download.ts
index c4eea8e8..85fb41be 100644
--- a/src/api/routes/download.ts
+++ b/src/api/routes/download.ts
@@ -16,32 +16,43 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
 import { FieldErrors, Release } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { platform } = req.query;
+router.get(
+	"/",
+	route({
+		responses: {
+			302: {},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { platform } = req.query;
+
+		if (!platform)
+			throw FieldErrors({
+				platform: {
+					code: "BASE_TYPE_REQUIRED",
+					message: req.t("common:field.BASE_TYPE_REQUIRED"),
+				},
+			});
 
-	if (!platform)
-		throw FieldErrors({
-			platform: {
-				code: "BASE_TYPE_REQUIRED",
-				message: req.t("common:field.BASE_TYPE_REQUIRED"),
+		const release = await Release.findOneOrFail({
+			where: {
+				enabled: true,
+				platform: platform as string,
 			},
+			order: { pub_date: "DESC" },
 		});
 
-	const release = await Release.findOneOrFail({
-		where: {
-			enabled: true,
-			platform: platform as string,
-		},
-		order: { pub_date: "DESC" },
-	});
-
-	res.redirect(release.url);
-});
+		res.redirect(release.url);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/gateway/bot.ts b/src/api/routes/gateway/bot.ts
index 243159ec..d9101159 100644
--- a/src/api/routes/gateway/bot.ts
+++ b/src/api/routes/gateway/bot.ts
@@ -16,32 +16,34 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
-import { Router, Response, Request } from "express";
-import { route, RouteOptions } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-const options: RouteOptions = {
-	test: {
-		response: {
-			body: "GatewayBotResponse",
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GatewayBotResponse",
+			},
 		},
+	}),
+	(req: Request, res: Response) => {
+		const { endpointPublic } = Config.get().gateway;
+		res.json({
+			url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
+			shards: 1,
+			session_start_limit: {
+				total: 1000,
+				remaining: 999,
+				reset_after: 14400000,
+				max_concurrency: 1,
+			},
+		});
 	},
-};
-
-router.get("/", route(options), (req: Request, res: Response) => {
-	const { endpointPublic } = Config.get().gateway;
-	res.json({
-		url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
-		shards: 1,
-		session_start_limit: {
-			total: 1000,
-			remaining: 999,
-			reset_after: 14400000,
-			max_concurrency: 1,
-		},
-	});
-});
+);
 
 export default router;
diff --git a/src/api/routes/gateway/index.ts b/src/api/routes/gateway/index.ts
index 12e96919..9100d5ee 100644
--- a/src/api/routes/gateway/index.ts
+++ b/src/api/routes/gateway/index.ts
@@ -16,25 +16,27 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
-import { Router, Response, Request } from "express";
-import { route, RouteOptions } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-const options: RouteOptions = {
-	test: {
-		response: {
-			body: "GatewayResponse",
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GatewayResponse",
+			},
 		},
+	}),
+	(req: Request, res: Response) => {
+		const { endpointPublic } = Config.get().gateway;
+		res.json({
+			url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
+		});
 	},
-};
-
-router.get("/", route(options), (req: Request, res: Response) => {
-	const { endpointPublic } = Config.get().gateway;
-	res.json({
-		url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
-	});
-});
+);
 
 export default router;
diff --git a/src/api/routes/gifs/search.ts b/src/api/routes/gifs/search.ts
index fb99374b..305a2a48 100644
--- a/src/api/routes/gifs/search.ts
+++ b/src/api/routes/gifs/search.ts
@@ -16,34 +16,62 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
-import fetch from "node-fetch";
-import ProxyAgent from "proxy-agent";
 import { route } from "@spacebar/api";
-import { getGifApiKey, parseGifResult } from "./trending";
+import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import fetch from "node-fetch";
+import { ProxyAgent } from "proxy-agent";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// TODO: Custom providers
-	const { q, media_format, locale } = req.query;
+router.get(
+	"/",
+	route({
+		query: {
+			q: {
+				type: "string",
+				required: true,
+				description: "Search query",
+			},
+			media_format: {
+				type: "string",
+				description: "Media format",
+				values: Object.keys(TenorMediaTypes).filter((key) =>
+					isNaN(Number(key)),
+				),
+			},
+			locale: {
+				type: "string",
+				description: "Locale",
+			},
+		},
+		responses: {
+			200: {
+				body: "TenorGifsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: Custom providers
+		const { q, media_format, locale } = req.query;
 
-	const apiKey = getGifApiKey();
+		const apiKey = getGifApiKey();
 
-	const agent = new ProxyAgent();
+		const agent = new ProxyAgent();
 
-	const response = await fetch(
-		`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`,
-		{
-			agent,
-			method: "get",
-			headers: { "Content-Type": "application/json" },
-		},
-	);
+		const response = await fetch(
+			`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`,
+			{
+				agent,
+				method: "get",
+				headers: { "Content-Type": "application/json" },
+			},
+		);
 
-	const { results } = await response.json();
+		const { results } = await response.json();
 
-	res.json(results.map(parseGifResult)).status(200);
-});
+		res.json(results.map(parseGifResult)).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/gifs/trending-gifs.ts b/src/api/routes/gifs/trending-gifs.ts
index 238a2abd..77a61efc 100644
--- a/src/api/routes/gifs/trending-gifs.ts
+++ b/src/api/routes/gifs/trending-gifs.ts
@@ -16,34 +16,57 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
-import fetch from "node-fetch";
-import ProxyAgent from "proxy-agent";
 import { route } from "@spacebar/api";
-import { getGifApiKey, parseGifResult } from "./trending";
+import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import fetch from "node-fetch";
+import { ProxyAgent } from "proxy-agent";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// TODO: Custom providers
-	const { media_format, locale } = req.query;
+router.get(
+	"/",
+	route({
+		query: {
+			media_format: {
+				type: "string",
+				description: "Media format",
+				values: Object.keys(TenorMediaTypes).filter((key) =>
+					isNaN(Number(key)),
+				),
+			},
+			locale: {
+				type: "string",
+				description: "Locale",
+			},
+		},
+		responses: {
+			200: {
+				body: "TenorGifsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: Custom providers
+		const { media_format, locale } = req.query;
 
-	const apiKey = getGifApiKey();
+		const apiKey = getGifApiKey();
 
-	const agent = new ProxyAgent();
+		const agent = new ProxyAgent();
 
-	const response = await fetch(
-		`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`,
-		{
-			agent,
-			method: "get",
-			headers: { "Content-Type": "application/json" },
-		},
-	);
+		const response = await fetch(
+			`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`,
+			{
+				agent,
+				method: "get",
+				headers: { "Content-Type": "application/json" },
+			},
+		);
 
-	const { results } = await response.json();
+		const { results } = await response.json();
 
-	res.json(results.map(parseGifResult)).status(200);
-});
+		res.json(results.map(parseGifResult)).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/gifs/trending.ts b/src/api/routes/gifs/trending.ts
index 5cccdb2d..fe726842 100644
--- a/src/api/routes/gifs/trending.ts
+++ b/src/api/routes/gifs/trending.ts
@@ -16,126 +16,76 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
-import fetch from "node-fetch";
-import ProxyAgent from "proxy-agent";
 import { route } from "@spacebar/api";
-import { Config } from "@spacebar/util";
-import { HTTPError } from "lambert-server";
+import {
+	TenorCategoriesResults,
+	TenorTrendingResults,
+	getGifApiKey,
+	parseGifResult,
+} from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import fetch from "node-fetch";
+import { ProxyAgent } from "proxy-agent";
 
 const router = Router();
 
-// TODO: Move somewhere else
-enum TENOR_GIF_TYPES {
-	gif,
-	mediumgif,
-	tinygif,
-	nanogif,
-	mp4,
-	loopedmp4,
-	tinymp4,
-	nanomp4,
-	webm,
-	tinywebm,
-	nanowebm,
-}
-
-type TENOR_MEDIA = {
-	preview: string;
-	url: string;
-	dims: number[];
-	size: number;
-};
-
-type TENOR_GIF = {
-	created: number;
-	hasaudio: boolean;
-	id: string;
-	media: { [type in keyof typeof TENOR_GIF_TYPES]: TENOR_MEDIA }[];
-	tags: string[];
-	title: string;
-	itemurl: string;
-	hascaption: boolean;
-	url: string;
-};
-
-type TENOR_CATEGORY = {
-	searchterm: string;
-	path: string;
-	image: string;
-	name: string;
-};
-
-type TENOR_CATEGORIES_RESULTS = {
-	tags: TENOR_CATEGORY[];
-};
-
-type TENOR_TRENDING_RESULTS = {
-	next: string;
-	results: TENOR_GIF[];
-};
-
-export function parseGifResult(result: TENOR_GIF) {
-	return {
-		id: result.id,
-		title: result.title,
-		url: result.itemurl,
-		src: result.media[0].mp4.url,
-		gif_src: result.media[0].gif.url,
-		width: result.media[0].mp4.dims[0],
-		height: result.media[0].mp4.dims[1],
-		preview: result.media[0].mp4.preview,
-	};
-}
-
-export function getGifApiKey() {
-	const { enabled, provider, apiKey } = Config.get().gif;
-	if (!enabled) throw new HTTPError(`Gifs are disabled`);
-	if (provider !== "tenor" || !apiKey)
-		throw new HTTPError(`${provider} gif provider not supported`);
-
-	return apiKey;
-}
-
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// TODO: Custom providers
-	// TODO: return gifs as mp4
-	// const { media_format, locale } = req.query;
-	const { locale } = req.query;
-
-	const apiKey = getGifApiKey();
-
-	const agent = new ProxyAgent();
-
-	const [responseSource, trendGifSource] = await Promise.all([
-		fetch(
-			`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`,
-			{
-				agent,
-				method: "get",
-				headers: { "Content-Type": "application/json" },
+router.get(
+	"/",
+	route({
+		query: {
+			locale: {
+				type: "string",
+				description: "Locale",
 			},
-		),
-		fetch(
-			`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`,
-			{
-				agent,
-				method: "get",
-				headers: { "Content-Type": "application/json" },
+		},
+		responses: {
+			200: {
+				body: "TenorTrendingResponse",
 			},
-		),
-	]);
-
-	const { tags } = (await responseSource.json()) as TENOR_CATEGORIES_RESULTS;
-	const { results } = (await trendGifSource.json()) as TENOR_TRENDING_RESULTS;
-
-	res.json({
-		categories: tags.map((x) => ({
-			name: x.searchterm,
-			src: x.image,
-		})),
-		gifs: [parseGifResult(results[0])],
-	}).status(200);
-});
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: Custom providers
+		// TODO: return gifs as mp4
+		// const { media_format, locale } = req.query;
+		const { locale } = req.query;
+
+		const apiKey = getGifApiKey();
+
+		const agent = new ProxyAgent();
+
+		const [responseSource, trendGifSource] = await Promise.all([
+			fetch(
+				`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`,
+				{
+					agent,
+					method: "get",
+					headers: { "Content-Type": "application/json" },
+				},
+			),
+			fetch(
+				`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`,
+				{
+					agent,
+					method: "get",
+					headers: { "Content-Type": "application/json" },
+				},
+			),
+		]);
+
+		const { tags } =
+			(await responseSource.json()) as TenorCategoriesResults;
+		const { results } =
+			(await trendGifSource.json()) as TenorTrendingResults;
+
+		res.json({
+			categories: tags.map((x) => ({
+				name: x.searchterm,
+				src: x.image,
+			})),
+			gifs: [parseGifResult(results[0])],
+		}).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guild-recommendations.ts b/src/api/routes/guild-recommendations.ts
index 67f43c14..876780df 100644
--- a/src/api/routes/guild-recommendations.ts
+++ b/src/api/routes/guild-recommendations.ts
@@ -16,34 +16,44 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Guild, Config } from "@spacebar/util";
+import { Config, Guild } from "@spacebar/util";
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { Like } from "typeorm";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// const { limit, personalization_disabled } = req.query;
-	const { limit } = req.query;
-	const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildRecommendationsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// const { limit, personalization_disabled } = req.query;
+		const { limit } = req.query;
+		const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
 
-	const genLoadId = (size: number) =>
-		[...Array(size)]
-			.map(() => Math.floor(Math.random() * 16).toString(16))
-			.join("");
+		const genLoadId = (size: number) =>
+			[...Array(size)]
+				.map(() => Math.floor(Math.random() * 16).toString(16))
+				.join("");
 
-	const guilds = showAllGuilds
-		? await Guild.find({ take: Math.abs(Number(limit || 24)) })
-		: await Guild.find({
-				where: { features: Like("%DISCOVERABLE%") },
-				take: Math.abs(Number(limit || 24)),
-		  });
-	res.send({
-		recommended_guilds: guilds,
-		load_id: `server_recs/${genLoadId(32)}`,
-	}).status(200);
-});
+		const guilds = showAllGuilds
+			? await Guild.find({ take: Math.abs(Number(limit || 24)) })
+			: await Guild.find({
+					where: { features: Like("%DISCOVERABLE%") },
+					take: Math.abs(Number(limit || 24)),
+			  });
+		res.send({
+			recommended_guilds: guilds,
+			load_id: `server_recs/${genLoadId(32)}`,
+		}).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts
index 31aed6b9..0776ab62 100644
--- a/src/api/routes/guilds/#guild_id/bans.ts
+++ b/src/api/routes/guilds/#guild_id/bans.ts
@@ -16,20 +16,20 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { getIpAdress, route } from "@spacebar/api";
 import {
+	Ban,
+	BanModeratorSchema,
+	BanRegistrySchema,
 	DiscordApiErrors,
-	emitEvent,
 	GuildBanAddEvent,
 	GuildBanRemoveEvent,
-	Ban,
-	User,
 	Member,
-	BanRegistrySchema,
-	BanModeratorSchema,
+	User,
+	emitEvent,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { getIpAdress, route } from "@spacebar/api";
 
 const router: Router = Router();
 
@@ -37,7 +37,17 @@ const router: Router = Router();
 
 router.get(
 	"/",
-	route({ permission: "BAN_MEMBERS" }),
+	route({
+		permission: "BAN_MEMBERS",
+		responses: {
+			200: {
+				body: "GuildBansResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 
@@ -73,7 +83,20 @@ router.get(
 
 router.get(
 	"/:user",
-	route({ permission: "BAN_MEMBERS" }),
+	route({
+		permission: "BAN_MEMBERS",
+		responses: {
+			200: {
+				body: "BanModeratorSchema",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const user_id = req.params.ban;
@@ -97,7 +120,21 @@ router.get(
 
 router.put(
 	"/:user_id",
-	route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }),
+	route({
+		requestBody: "BanCreateSchema",
+		permission: "BAN_MEMBERS",
+		responses: {
+			200: {
+				body: "Ban",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const banned_user_id = req.params.user_id;
@@ -143,7 +180,20 @@ router.put(
 
 router.put(
 	"/@me",
-	route({ body: "BanCreateSchema" }),
+	route({
+		requestBody: "BanCreateSchema",
+		responses: {
+			200: {
+				body: "Ban",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 
@@ -182,7 +232,18 @@ router.put(
 
 router.delete(
 	"/:user_id",
-	route({ permission: "BAN_MEMBERS" }),
+	route({
+		permission: "BAN_MEMBERS",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, user_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/channels.ts b/src/api/routes/guilds/#guild_id/channels.ts
index d74d9f84..1d5897a5 100644
--- a/src/api/routes/guilds/#guild_id/channels.ts
+++ b/src/api/routes/guilds/#guild_id/channels.ts
@@ -16,28 +16,52 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
-	ChannelUpdateEvent,
-	emitEvent,
 	ChannelModifySchema,
 	ChannelReorderSchema,
+	ChannelUpdateEvent,
+	emitEvent,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	const channels = await Channel.find({ where: { guild_id } });
+router.get(
+	"/",
+	route({
+		responses: {
+			201: {
+				body: "APIChannelArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		const channels = await Channel.find({ where: { guild_id } });
 
-	res.json(channels);
-});
+		res.json(channels);
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }),
+	route({
+		requestBody: "ChannelModifySchema",
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			201: {
+				body: "Channel",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		// creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel
 		const { guild_id } = req.params;
@@ -54,7 +78,19 @@ router.post(
 
 router.patch(
 	"/",
-	route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }),
+	route({
+		requestBody: "ChannelReorderSchema",
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		// changes guild channel position
 		const { guild_id } = req.params;
diff --git a/src/api/routes/guilds/#guild_id/delete.ts b/src/api/routes/guilds/#guild_id/delete.ts
index ec72a4ae..dee52c81 100644
--- a/src/api/routes/guilds/#guild_id/delete.ts
+++ b/src/api/routes/guilds/#guild_id/delete.ts
@@ -16,37 +16,51 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { emitEvent, GuildDeleteEvent, Guild } from "@spacebar/util";
-import { Router, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
 import { route } from "@spacebar/api";
+import { Guild, GuildDeleteEvent, emitEvent } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
 
 const router = Router();
 
 // discord prefixes this route with /delete instead of using the delete method
 // docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild
-router.post("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+			401: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	const guild = await Guild.findOneOrFail({
-		where: { id: guild_id },
-		select: ["owner_id"],
-	});
-	if (guild.owner_id !== req.user_id)
-		throw new HTTPError("You are not the owner of this guild", 401);
+		const guild = await Guild.findOneOrFail({
+			where: { id: guild_id },
+			select: ["owner_id"],
+		});
+		if (guild.owner_id !== req.user_id)
+			throw new HTTPError("You are not the owner of this guild", 401);
 
-	await Promise.all([
-		Guild.delete({ id: guild_id }), // this will also delete all guild related data
-		emitEvent({
-			event: "GUILD_DELETE",
-			data: {
-				id: guild_id,
-			},
-			guild_id: guild_id,
-		} as GuildDeleteEvent),
-	]);
+		await Promise.all([
+			Guild.delete({ id: guild_id }), // this will also delete all guild related data
+			emitEvent({
+				event: "GUILD_DELETE",
+				data: {
+					id: guild_id,
+				},
+				guild_id: guild_id,
+			} as GuildDeleteEvent),
+		]);
 
-	return res.sendStatus(204);
-});
+		return res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/discovery-requirements.ts b/src/api/routes/guilds/#guild_id/discovery-requirements.ts
index 5e15676a..741fa9b3 100644
--- a/src/api/routes/guilds/#guild_id/discovery-requirements.ts
+++ b/src/api/routes/guilds/#guild_id/discovery-requirements.ts
@@ -16,40 +16,50 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	// TODO:
-	// Load from database
-	// Admin control, but for now it allows anyone to be discoverable
-
-	res.send({
-		guild_id: guild_id,
-		safe_environment: true,
-		healthy: true,
-		health_score_pending: false,
-		size: true,
-		nsfw_properties: {},
-		protected: true,
-		sufficient: true,
-		sufficient_without_grace_period: true,
-		valid_rules_channel: true,
-		retention_healthy: true,
-		engagement_healthy: true,
-		age: true,
-		minimum_age: 0,
-		health_score: {
-			avg_nonnew_participators: 0,
-			avg_nonnew_communicators: 0,
-			num_intentful_joiners: 0,
-			perc_ret_w1_intentful: 0,
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildDiscoveryRequirementsResponse",
+			},
 		},
-		minimum_size: 0,
-	});
-});
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		// TODO:
+		// Load from database
+		// Admin control, but for now it allows anyone to be discoverable
+
+		res.send({
+			guild_id: guild_id,
+			safe_environment: true,
+			healthy: true,
+			health_score_pending: false,
+			size: true,
+			nsfw_properties: {},
+			protected: true,
+			sufficient: true,
+			sufficient_without_grace_period: true,
+			valid_rules_channel: true,
+			retention_healthy: true,
+			engagement_healthy: true,
+			age: true,
+			minimum_age: 0,
+			health_score: {
+				avg_nonnew_participators: 0,
+				avg_nonnew_communicators: 0,
+				num_intentful_joiners: 0,
+				perc_ret_w1_intentful: 0,
+			},
+			minimum_size: 0,
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/emojis.ts b/src/api/routes/guilds/#guild_id/emojis.ts
index c661202e..ef28f989 100644
--- a/src/api/routes/guilds/#guild_id/emojis.ts
+++ b/src/api/routes/guilds/#guild_id/emojis.ts
@@ -16,55 +16,95 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
 	Config,
 	DiscordApiErrors,
-	emitEvent,
 	Emoji,
+	EmojiCreateSchema,
+	EmojiModifySchema,
 	GuildEmojisUpdateEvent,
-	handleFile,
 	Member,
 	Snowflake,
 	User,
-	EmojiCreateSchema,
-	EmojiModifySchema,
+	emitEvent,
+	handleFile,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIEmojiArray",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	const emojis = await Emoji.find({
-		where: { guild_id: guild_id },
-		relations: ["user"],
-	});
+		const emojis = await Emoji.find({
+			where: { guild_id: guild_id },
+			relations: ["user"],
+		});
 
-	return res.json(emojis);
-});
+		return res.json(emojis);
+	},
+);
 
-router.get("/:emoji_id", route({}), async (req: Request, res: Response) => {
-	const { guild_id, emoji_id } = req.params;
+router.get(
+	"/:emoji_id",
+	route({
+		responses: {
+			200: {
+				body: "Emoji",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, emoji_id } = req.params;
 
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	const emoji = await Emoji.findOneOrFail({
-		where: { guild_id: guild_id, id: emoji_id },
-		relations: ["user"],
-	});
+		const emoji = await Emoji.findOneOrFail({
+			where: { guild_id: guild_id, id: emoji_id },
+			relations: ["user"],
+		});
 
-	return res.json(emoji);
-});
+		return res.json(emoji);
+	},
+);
 
 router.post(
 	"/",
 	route({
-		body: "EmojiCreateSchema",
+		requestBody: "EmojiCreateSchema",
 		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			201: {
+				body: "Emoji",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
@@ -113,8 +153,16 @@ router.post(
 router.patch(
 	"/:emoji_id",
 	route({
-		body: "EmojiModifySchema",
+		requestBody: "EmojiModifySchema",
 		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			200: {
+				body: "Emoji",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { emoji_id, guild_id } = req.params;
@@ -141,7 +189,15 @@ router.patch(
 
 router.delete(
 	"/:emoji_id",
-	route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }),
+	route({
+		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { emoji_id, guild_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts
index 672bc92e..6feb0a83 100644
--- a/src/api/routes/guilds/#guild_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/index.ts
@@ -16,46 +16,81 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
+	Channel,
 	DiscordApiErrors,
-	emitEvent,
-	getPermission,
-	getRights,
 	Guild,
 	GuildUpdateEvent,
-	handleFile,
-	Member,
 	GuildUpdateSchema,
+	Member,
+	Permissions,
 	SpacebarApiErrors,
+	emitEvent,
+	getPermission,
+	getRights,
+	handleFile,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-
-	const [guild, member] = await Promise.all([
-		Guild.findOneOrFail({ where: { id: guild_id } }),
-		Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }),
-	]);
-	if (!member)
-		throw new HTTPError(
-			"You are not a member of the guild you are trying to access",
-			401,
-		);
-
-	return res.send({
-		...guild,
-		joined_at: member?.joined_at,
-	});
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			"200": {
+				body: "APIGuildWithJoinedAt",
+			},
+			401: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+
+		const [guild, member] = await Promise.all([
+			Guild.findOneOrFail({ where: { id: guild_id } }),
+			Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }),
+		]);
+		if (!member)
+			throw new HTTPError(
+				"You are not a member of the guild you are trying to access",
+				401,
+			);
+
+		return res.send({
+			...guild,
+			joined_at: member?.joined_at,
+		});
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "GuildUpdateSchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "GuildUpdateSchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			"200": {
+				body: "GuildUpdateSchema",
+			},
+			401: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as GuildUpdateSchema;
 		const { guild_id } = req.params;
@@ -122,13 +157,83 @@ router.patch(
 			guild.features = body.features;
 		}
 
+		if (body.public_updates_channel_id == "1") {
+			// move all channels up 1
+			await Channel.createQueryBuilder("channels")
+				.where({ guild: { id: guild_id } })
+				.update({ position: () => "position + 1" })
+				.execute();
+
+			// create an updates channel for them
+			await Channel.createChannel(
+				{
+					name: "moderator-only",
+					guild_id: guild.id,
+					position: 0,
+					type: 0,
+					permission_overwrites: [
+						// remove SEND_MESSAGES from @everyone
+						{
+							id: guild.id,
+							allow: "0",
+							deny: Permissions.FLAGS.VIEW_CHANNEL.toString(),
+							type: 0,
+						},
+					],
+				},
+				undefined,
+				{ skipPermissionCheck: true },
+			);
+		} else if (body.public_updates_channel_id != undefined) {
+			// ensure channel exists in this guild
+			await Channel.findOneOrFail({
+				where: { guild_id, id: body.public_updates_channel_id },
+				select: { id: true },
+			});
+		}
+
+		if (body.rules_channel_id == "1") {
+			// move all channels up 1
+			await Channel.createQueryBuilder("channels")
+				.where({ guild: { id: guild_id } })
+				.update({ position: () => "position + 1" })
+				.execute();
+
+			// create a rules for them
+			await Channel.createChannel(
+				{
+					name: "rules",
+					guild_id: guild.id,
+					position: 0,
+					type: 0,
+					permission_overwrites: [
+						// remove SEND_MESSAGES from @everyone
+						{
+							id: guild.id,
+							allow: "0",
+							deny: Permissions.FLAGS.SEND_MESSAGES.toString(),
+							type: 0,
+						},
+					],
+				},
+				undefined,
+				{ skipPermissionCheck: true },
+			);
+		} else if (body.rules_channel_id != undefined) {
+			// ensure channel exists in this guild
+			await Channel.findOneOrFail({
+				where: { guild_id, id: body.rules_channel_id },
+				select: { id: true },
+			});
+		}
+
 		// TODO: check if body ids are valid
 		guild.assign(body);
 
 		const data = guild.toJSON();
 		// TODO: guild hashes
 		// TODO: fix vanity_url_code, template_id
-		delete data.vanity_url_code;
+		// delete data.vanity_url_code;
 		delete data.template_id;
 
 		await Promise.all([
diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts
index 9c446928..a0ffa3f4 100644
--- a/src/api/routes/guilds/#guild_id/invites.ts
+++ b/src/api/routes/guilds/#guild_id/invites.ts
@@ -16,15 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Invite, PublicInviteRelation } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { Invite, PublicInviteRelation } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.get(
 	"/",
-	route({ permission: "MANAGE_GUILD" }),
+	route({
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "APIInviteArray",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/member-verification.ts b/src/api/routes/guilds/#guild_id/member-verification.ts
index 242f3684..2c39093e 100644
--- a/src/api/routes/guilds/#guild_id/member-verification.ts
+++ b/src/api/routes/guilds/#guild_id/member-verification.ts
@@ -16,17 +16,27 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// TODO: member verification
+router.get(
+	"/",
+	route({
+		responses: {
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: member verification
 
-	res.status(404).json({
-		message: "Unknown Guild Member Verification Form",
-		code: 10068,
-	});
-});
+		res.status(404).json({
+			message: "Unknown Guild Member Verification Form",
+			code: 10068,
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
index a14691f2..cafb922e 100644
--- a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -16,38 +16,91 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
-	Member,
-	getPermission,
-	getRights,
-	Role,
-	GuildMemberUpdateEvent,
 	emitEvent,
-	Sticker,
 	Emoji,
+	getPermission,
+	getRights,
 	Guild,
+	GuildMemberUpdateEvent,
 	handleFile,
+	Member,
 	MemberChangeSchema,
+	PublicMemberProjection,
+	PublicUserProjection,
+	Role,
+	Sticker,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id, member_id } = req.params;
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIPublicMember",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, member_id } = req.params;
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	const member = await Member.findOneOrFail({
-		where: { id: member_id, guild_id },
-	});
+		const member = await Member.findOneOrFail({
+			where: { id: member_id, guild_id },
+			relations: ["roles", "user"],
+			select: {
+				index: true,
+				// only grab public member props
+				...Object.fromEntries(
+					PublicMemberProjection.map((x) => [x, true]),
+				),
+				// and public user props
+				user: Object.fromEntries(
+					PublicUserProjection.map((x) => [x, true]),
+				),
+				roles: {
+					id: true,
+				},
+			},
+		});
 
-	return res.json(member);
-});
+		return res.json({
+			...member.toPublicMember(),
+			user: member.user.toPublicUser(),
+			roles: member.roles.map((x) => x.id),
+		});
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "MemberChangeSchema" }),
+	route({
+		requestBody: "MemberChangeSchema",
+		responses: {
+			200: {
+				body: "Member",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const member_id =
@@ -119,54 +172,81 @@ router.patch(
 	},
 );
 
-router.put("/", route({}), async (req: Request, res: Response) => {
-	// TODO: Lurker mode
-
-	const rights = await getRights(req.user_id);
-
-	const { guild_id } = req.params;
-	let { member_id } = req.params;
-	if (member_id === "@me") {
-		member_id = req.user_id;
-		rights.hasThrow("JOIN_GUILDS");
-	} else {
-		// TODO: join others by controller
-	}
-
-	const guild = await Guild.findOneOrFail({
-		where: { id: guild_id },
-	});
-
-	const emoji = await Emoji.find({
-		where: { guild_id: guild_id },
-	});
-
-	const roles = await Role.find({
-		where: { guild_id: guild_id },
-	});
-
-	const stickers = await Sticker.find({
-		where: { guild_id: guild_id },
-	});
-
-	await Member.addToGuild(member_id, guild_id);
-	res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers });
-});
-
-router.delete("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id, member_id } = req.params;
-	const permission = await getPermission(req.user_id, guild_id);
-	const rights = await getRights(req.user_id);
-	if (member_id === "@me" || member_id === req.user_id) {
-		// TODO: unless force-joined
-		rights.hasThrow("SELF_LEAVE_GROUPS");
-	} else {
-		rights.hasThrow("KICK_BAN_MEMBERS");
-		permission.hasThrow("KICK_MEMBERS");
-	}
-
-	await Member.removeFromGuild(member_id, guild_id);
-	res.sendStatus(204);
-});
+router.put(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "MemberJoinGuildResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: Lurker mode
+
+		const rights = await getRights(req.user_id);
+
+		const { guild_id } = req.params;
+		let { member_id } = req.params;
+		if (member_id === "@me") {
+			member_id = req.user_id;
+			rights.hasThrow("JOIN_GUILDS");
+		} else {
+			// TODO: join others by controller
+		}
+
+		const guild = await Guild.findOneOrFail({
+			where: { id: guild_id },
+		});
+
+		const emoji = await Emoji.find({
+			where: { guild_id: guild_id },
+		});
+
+		const roles = await Role.find({
+			where: { guild_id: guild_id },
+		});
+
+		const stickers = await Sticker.find({
+			where: { guild_id: guild_id },
+		});
+
+		await Member.addToGuild(member_id, guild_id);
+		res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers });
+	},
+);
+
+router.delete(
+	"/",
+	route({
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, member_id } = req.params;
+		const permission = await getPermission(req.user_id, guild_id);
+		const rights = await getRights(req.user_id);
+		if (member_id === "@me" || member_id === req.user_id) {
+			// TODO: unless force-joined
+			rights.hasThrow("SELF_LEAVE_GROUPS");
+		} else {
+			rights.hasThrow("KICK_BAN_MEMBERS");
+			permission.hasThrow("KICK_MEMBERS");
+		}
+
+		await Member.removeFromGuild(member_id, guild_id);
+		res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
index 14e7467f..7b8e44d3 100644
--- a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
@@ -16,15 +16,26 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { getPermission, Member, PermissionResolvable } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { getPermission, Member, PermissionResolvable } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.patch(
 	"/",
-	route({ body: "MemberNickChangeSchema" }),
+	route({
+		requestBody: "MemberNickChangeSchema",
+		responses: {
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		let permissionString: PermissionResolvable = "MANAGE_NICKNAMES";
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
index 698df88f..46dd70bb 100644
--- a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
@@ -16,15 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Member } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { Member } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_ROLES" }),
+	route({
+		permission: "MANAGE_ROLES",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, role_id, member_id } = req.params;
 
@@ -35,7 +43,13 @@ router.delete(
 
 router.put(
 	"/",
-	route({ permission: "MANAGE_ROLES" }),
+	route({
+		permission: "MANAGE_ROLES",
+		responses: {
+			204: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, role_id, member_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/members/index.ts b/src/api/routes/guilds/#guild_id/members/index.ts
index f7a55cf1..9260308d 100644
--- a/src/api/routes/guilds/#guild_id/members/index.ts
+++ b/src/api/routes/guilds/#guild_id/members/index.ts
@@ -16,35 +16,58 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
-import { Member, PublicMemberProjection } from "@spacebar/util";
 import { route } from "@spacebar/api";
-import { MoreThan } from "typeorm";
+import { Member, PublicMemberProjection } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import { MoreThan } from "typeorm";
 
 const router = Router();
 
 // TODO: send over websocket
 // TODO: check for GUILD_MEMBERS intent
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	const limit = Number(req.query.limit) || 1;
-	if (limit > 1000 || limit < 1)
-		throw new HTTPError("Limit must be between 1 and 1000");
-	const after = `${req.query.after}`;
-	const query = after ? { id: MoreThan(after) } : {};
-
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
-
-	const members = await Member.find({
-		where: { guild_id, ...query },
-		select: PublicMemberProjection,
-		take: limit,
-		order: { id: "ASC" },
-	});
-
-	return res.json(members);
-});
+router.get(
+	"/",
+	route({
+		query: {
+			limit: {
+				type: "number",
+				description:
+					"max number of members to return (1-1000). default 1",
+			},
+			after: {
+				type: "string",
+			},
+		},
+		responses: {
+			200: {
+				body: "APIMemberArray",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		const limit = Number(req.query.limit) || 1;
+		if (limit > 1000 || limit < 1)
+			throw new HTTPError("Limit must be between 1 and 1000");
+		const after = `${req.query.after}`;
+		const query = after ? { id: MoreThan(after) } : {};
+
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+		const members = await Member.find({
+			where: { guild_id, ...query },
+			select: PublicMemberProjection,
+			take: limit,
+			order: { id: "ASC" },
+		});
+
+		return res.json(members);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/messages/search.ts b/src/api/routes/guilds/#guild_id/messages/search.ts
index bc5f1b6e..637d1e43 100644
--- a/src/api/routes/guilds/#guild_id/messages/search.ts
+++ b/src/api/routes/guilds/#guild_id/messages/search.ts
@@ -18,140 +18,159 @@
 
 /* eslint-disable @typescript-eslint/ban-ts-comment */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
-import { getPermission, FieldErrors, Message, Channel } from "@spacebar/util";
+import { Channel, FieldErrors, Message, getPermission } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { FindManyOptions, In, Like } from "typeorm";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const {
-		channel_id,
-		content,
-		// include_nsfw, // TODO
-		offset,
-		sort_order,
-		// sort_by, // TODO: Handle 'relevance'
-		limit,
-		author_id,
-	} = req.query;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildMessagesSearchResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			422: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const {
+			channel_id,
+			content,
+			// include_nsfw, // TODO
+			offset,
+			sort_order,
+			// sort_by, // TODO: Handle 'relevance'
+			limit,
+			author_id,
+		} = req.query;
 
-	const parsedLimit = Number(limit) || 50;
-	if (parsedLimit < 1 || parsedLimit > 100)
-		throw new HTTPError("limit must be between 1 and 100", 422);
+		const parsedLimit = Number(limit) || 50;
+		if (parsedLimit < 1 || parsedLimit > 100)
+			throw new HTTPError("limit must be between 1 and 100", 422);
 
-	if (sort_order) {
-		if (
-			typeof sort_order != "string" ||
-			["desc", "asc"].indexOf(sort_order) == -1
-		)
-			throw FieldErrors({
-				sort_order: {
-					message: "Value must be one of ('desc', 'asc').",
-					code: "BASE_TYPE_CHOICES",
-				},
-			}); // todo this is wrong
-	}
+		if (sort_order) {
+			if (
+				typeof sort_order != "string" ||
+				["desc", "asc"].indexOf(sort_order) == -1
+			)
+				throw FieldErrors({
+					sort_order: {
+						message: "Value must be one of ('desc', 'asc').",
+						code: "BASE_TYPE_CHOICES",
+					},
+				}); // todo this is wrong
+		}
 
-	const permissions = await getPermission(
-		req.user_id,
-		req.params.guild_id,
-		channel_id as string | undefined,
-	);
-	permissions.hasThrow("VIEW_CHANNEL");
-	if (!permissions.has("READ_MESSAGE_HISTORY"))
-		return res.json({ messages: [], total_results: 0 });
+		const permissions = await getPermission(
+			req.user_id,
+			req.params.guild_id,
+			channel_id as string | undefined,
+		);
+		permissions.hasThrow("VIEW_CHANNEL");
+		if (!permissions.has("READ_MESSAGE_HISTORY"))
+			return res.json({ messages: [], total_results: 0 });
 
-	const query: FindManyOptions<Message> = {
-		order: {
-			timestamp: sort_order
-				? (sort_order.toUpperCase() as "ASC" | "DESC")
-				: "DESC",
-		},
-		take: parsedLimit || 0,
-		where: {
-			guild: {
-				id: req.params.guild_id,
+		const query: FindManyOptions<Message> = {
+			order: {
+				timestamp: sort_order
+					? (sort_order.toUpperCase() as "ASC" | "DESC")
+					: "DESC",
 			},
-		},
-		relations: [
-			"author",
-			"webhook",
-			"application",
-			"mentions",
-			"mention_roles",
-			"mention_channels",
-			"sticker_items",
-			"attachments",
-		],
-		skip: offset ? Number(offset) : 0,
-	};
-	//@ts-ignore
-	if (channel_id) query.where.channel = { id: channel_id };
-	else {
-		// get all channel IDs that this user can access
-		const channels = await Channel.find({
-			where: { guild_id: req.params.guild_id },
-			select: ["id"],
-		});
-		const ids = [];
+			take: parsedLimit || 0,
+			where: {
+				guild: {
+					id: req.params.guild_id,
+				},
+			},
+			relations: [
+				"author",
+				"webhook",
+				"application",
+				"mentions",
+				"mention_roles",
+				"mention_channels",
+				"sticker_items",
+				"attachments",
+			],
+			skip: offset ? Number(offset) : 0,
+		};
+		//@ts-ignore
+		if (channel_id) query.where.channel = { id: channel_id };
+		else {
+			// get all channel IDs that this user can access
+			const channels = await Channel.find({
+				where: { guild_id: req.params.guild_id },
+				select: ["id"],
+			});
+			const ids = [];
 
-		for (const channel of channels) {
-			const perm = await getPermission(
-				req.user_id,
-				req.params.guild_id,
-				channel.id,
-			);
-			if (!perm.has("VIEW_CHANNEL") || !perm.has("READ_MESSAGE_HISTORY"))
-				continue;
-			ids.push(channel.id);
-		}
+			for (const channel of channels) {
+				const perm = await getPermission(
+					req.user_id,
+					req.params.guild_id,
+					channel.id,
+				);
+				if (
+					!perm.has("VIEW_CHANNEL") ||
+					!perm.has("READ_MESSAGE_HISTORY")
+				)
+					continue;
+				ids.push(channel.id);
+			}
 
+			//@ts-ignore
+			query.where.channel = { id: In(ids) };
+		}
+		//@ts-ignore
+		if (author_id) query.where.author = { id: author_id };
 		//@ts-ignore
-		query.where.channel = { id: In(ids) };
-	}
-	//@ts-ignore
-	if (author_id) query.where.author = { id: author_id };
-	//@ts-ignore
-	if (content) query.where.content = Like(`%${content}%`);
+		if (content) query.where.content = Like(`%${content}%`);
 
-	const messages: Message[] = await Message.find(query);
+		const messages: Message[] = await Message.find(query);
 
-	const messagesDto = messages.map((x) => [
-		{
-			id: x.id,
-			type: x.type,
-			content: x.content,
-			channel_id: x.channel_id,
-			author: {
-				id: x.author?.id,
-				username: x.author?.username,
-				avatar: x.author?.avatar,
-				avatar_decoration: null,
-				discriminator: x.author?.discriminator,
-				public_flags: x.author?.public_flags,
+		const messagesDto = messages.map((x) => [
+			{
+				id: x.id,
+				type: x.type,
+				content: x.content,
+				channel_id: x.channel_id,
+				author: {
+					id: x.author?.id,
+					username: x.author?.username,
+					avatar: x.author?.avatar,
+					avatar_decoration: null,
+					discriminator: x.author?.discriminator,
+					public_flags: x.author?.public_flags,
+				},
+				attachments: x.attachments,
+				embeds: x.embeds,
+				mentions: x.mentions,
+				mention_roles: x.mention_roles,
+				pinned: x.pinned,
+				mention_everyone: x.mention_everyone,
+				tts: x.tts,
+				timestamp: x.timestamp,
+				edited_timestamp: x.edited_timestamp,
+				flags: x.flags,
+				components: x.components,
+				hit: true,
 			},
-			attachments: x.attachments,
-			embeds: x.embeds,
-			mentions: x.mentions,
-			mention_roles: x.mention_roles,
-			pinned: x.pinned,
-			mention_everyone: x.mention_everyone,
-			tts: x.tts,
-			timestamp: x.timestamp,
-			edited_timestamp: x.edited_timestamp,
-			flags: x.flags,
-			components: x.components,
-			hit: true,
-		},
-	]);
+		]);
 
-	return res.json({
-		messages: messagesDto,
-		total_results: messages.length,
-	});
-});
+		return res.json({
+			messages: messagesDto,
+			total_results: messages.length,
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/profile/index.ts b/src/api/routes/guilds/#guild_id/profile/index.ts
index 8ec22ea4..60526259 100644
--- a/src/api/routes/guilds/#guild_id/profile/index.ts
+++ b/src/api/routes/guilds/#guild_id/profile/index.ts
@@ -31,7 +31,20 @@ const router = Router();
 
 router.patch(
 	"/:member_id",
-	route({ body: "MemberChangeProfileSchema" }),
+	route({
+		requestBody: "MemberChangeProfileSchema",
+		responses: {
+			200: {
+				body: "Member",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		// const member_id =
diff --git a/src/api/routes/guilds/#guild_id/prune.ts b/src/api/routes/guilds/#guild_id/prune.ts
index dbed546b..2c77340d 100644
--- a/src/api/routes/guilds/#guild_id/prune.ts
+++ b/src/api/routes/guilds/#guild_id/prune.ts
@@ -16,14 +16,14 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { Guild, Member, Snowflake } from "@spacebar/util";
-import { LessThan, IsNull } from "typeorm";
 import { route } from "@spacebar/api";
+import { Guild, Member, Snowflake } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { IsNull, LessThan } from "typeorm";
 const router = Router();
 
 //Returns all inactive members, respecting role hierarchy
-export const inactiveMembers = async (
+const inactiveMembers = async (
 	guild_id: string,
 	user_id: string,
 	days: number,
@@ -80,25 +80,46 @@ export const inactiveMembers = async (
 	return members;
 };
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const days = parseInt(req.query.days as string);
+router.get(
+	"/",
+	route({
+		responses: {
+			"200": {
+				body: "GuildPruneResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const days = parseInt(req.query.days as string);
 
-	let roles = req.query.include_roles;
-	if (typeof roles === "string") roles = [roles]; //express will return array otherwise
+		let roles = req.query.include_roles;
+		if (typeof roles === "string") roles = [roles]; //express will return array otherwise
 
-	const members = await inactiveMembers(
-		req.params.guild_id,
-		req.user_id,
-		days,
-		roles as string[],
-	);
+		const members = await inactiveMembers(
+			req.params.guild_id,
+			req.user_id,
+			days,
+			roles as string[],
+		);
 
-	res.send({ pruned: members.length });
-});
+		res.send({ pruned: members.length });
+	},
+);
 
 router.post(
 	"/",
-	route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }),
+	route({
+		permission: "KICK_MEMBERS",
+		right: "KICK_BAN_MEMBERS",
+		responses: {
+			200: {
+				body: "GuildPurgeResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const days = parseInt(req.body.days);
 
diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts
index de1e8769..b0ae0602 100644
--- a/src/api/routes/guilds/#guild_id/regions.ts
+++ b/src/api/routes/guilds/#guild_id/regions.ts
@@ -16,22 +16,35 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
 import { Guild } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { getVoiceRegions, route, getIpAdress } from "@spacebar/api";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
-	//TODO we should use an enum for guild's features and not hardcoded strings
-	return res.json(
-		await getVoiceRegions(
-			getIpAdress(req),
-			guild.features.includes("VIP_REGIONS"),
-		),
-	);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIGuildVoiceRegion",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		//TODO we should use an enum for guild's features and not hardcoded strings
+		return res.json(
+			await getVoiceRegions(
+				getIpAdress(req),
+				guild.features.includes("VIP_REGIONS"),
+			),
+		);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
index de3fc35b..ea1a782a 100644
--- a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
@@ -16,31 +16,63 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
-	Role,
-	Member,
-	GuildRoleUpdateEvent,
-	GuildRoleDeleteEvent,
 	emitEvent,
+	GuildRoleDeleteEvent,
+	GuildRoleUpdateEvent,
 	handleFile,
+	Member,
+	Role,
 	RoleModifySchema,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id, role_id } = req.params;
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
-	const role = await Role.findOneOrFail({ where: { guild_id, id: role_id } });
-	return res.json(role);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "Role",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, role_id } = req.params;
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
+		const role = await Role.findOneOrFail({
+			where: { guild_id, id: role_id },
+		});
+		return res.json(role);
+	},
+);
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_ROLES" }),
+	route({
+		permission: "MANAGE_ROLES",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, role_id } = req.params;
 		if (role_id === guild_id)
@@ -69,7 +101,24 @@ router.delete(
 
 router.patch(
 	"/",
-	route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }),
+	route({
+		requestBody: "RoleModifySchema",
+		permission: "MANAGE_ROLES",
+		responses: {
+			200: {
+				body: "Role",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { role_id, guild_id } = req.params;
 		const body = req.body as RoleModifySchema;
diff --git a/src/api/routes/guilds/#guild_id/roles/index.ts b/src/api/routes/guilds/#guild_id/roles/index.ts
index f93e9385..e2c34e7f 100644
--- a/src/api/routes/guilds/#guild_id/roles/index.ts
+++ b/src/api/routes/guilds/#guild_id/roles/index.ts
@@ -16,21 +16,20 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
-	Role,
-	getPermission,
-	Member,
-	GuildRoleCreateEvent,
-	GuildRoleUpdateEvent,
-	emitEvent,
 	Config,
 	DiscordApiErrors,
+	emitEvent,
+	GuildRoleCreateEvent,
+	GuildRoleUpdateEvent,
+	Member,
+	Role,
 	RoleModifySchema,
 	RolePositionUpdateSchema,
 	Snowflake,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { Not } from "typeorm";
 
 const router: Router = Router();
@@ -47,7 +46,21 @@ router.get("/", route({}), async (req: Request, res: Response) => {
 
 router.post(
 	"/",
-	route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }),
+	route({
+		requestBody: "RoleModifySchema",
+		permission: "MANAGE_ROLES",
+		responses: {
+			200: {
+				body: "Role",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const guild_id = req.params.guild_id;
 		const body = req.body as RoleModifySchema;
@@ -104,14 +117,25 @@ router.post(
 
 router.patch(
 	"/",
-	route({ body: "RolePositionUpdateSchema" }),
+	route({
+		requestBody: "RolePositionUpdateSchema",
+		permission: "MANAGE_ROLES",
+		responses: {
+			200: {
+				body: "APIRoleArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const body = req.body as RolePositionUpdateSchema;
 
-		const perms = await getPermission(req.user_id, guild_id);
-		perms.hasThrow("MANAGE_ROLES");
-
 		await Promise.all(
 			body.map(async (x) =>
 				Role.update({ guild_id, id: x.id }, { position: x.position }),
diff --git a/src/api/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts
index 84a23670..88f9a40e 100644
--- a/src/api/routes/guilds/#guild_id/stickers.ts
+++ b/src/api/routes/guilds/#guild_id/stickers.ts
@@ -16,29 +16,42 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
-	emitEvent,
 	GuildStickersUpdateEvent,
 	Member,
+	ModifyGuildStickerSchema,
 	Snowflake,
 	Sticker,
 	StickerFormatType,
 	StickerType,
+	emitEvent,
 	uploadFile,
-	ModifyGuildStickerSchema,
 } from "@spacebar/util";
-import { Router, Request, Response } from "express";
-import { route } from "@spacebar/api";
-import multer from "multer";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import multer from "multer";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIStickerArray",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	res.json(await Sticker.find({ where: { guild_id } }));
-});
+		res.json(await Sticker.find({ where: { guild_id } }));
+	},
+);
 
 const bodyParser = multer({
 	limits: {
@@ -54,7 +67,18 @@ router.post(
 	bodyParser,
 	route({
 		permission: "MANAGE_EMOJIS_AND_STICKERS",
-		body: "ModifyGuildStickerSchema",
+		requestBody: "ModifyGuildStickerSchema",
+		responses: {
+			200: {
+				body: "Sticker",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		if (!req.file) throw new HTTPError("missing file");
@@ -81,7 +105,7 @@ router.post(
 	},
 );
 
-export function getStickerFormat(mime_type: string) {
+function getStickerFormat(mime_type: string) {
 	switch (mime_type) {
 		case "image/apng":
 			return StickerFormatType.APNG;
@@ -98,20 +122,46 @@ export function getStickerFormat(mime_type: string) {
 	}
 }
 
-router.get("/:sticker_id", route({}), async (req: Request, res: Response) => {
-	const { guild_id, sticker_id } = req.params;
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+router.get(
+	"/:sticker_id",
+	route({
+		responses: {
+			200: {
+				body: "Sticker",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, sticker_id } = req.params;
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	res.json(
-		await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } }),
-	);
-});
+		res.json(
+			await Sticker.findOneOrFail({
+				where: { guild_id, id: sticker_id },
+			}),
+		);
+	},
+);
 
 router.patch(
 	"/:sticker_id",
 	route({
-		body: "ModifyGuildStickerSchema",
+		requestBody: "ModifyGuildStickerSchema",
 		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			200: {
+				body: "Sticker",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, sticker_id } = req.params;
@@ -141,7 +191,15 @@ async function sendStickerUpdateEvent(guild_id: string) {
 
 router.delete(
 	"/:sticker_id",
-	route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }),
+	route({
+		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, sticker_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts
index 3bd28e05..85ae0ac9 100644
--- a/src/api/routes/guilds/#guild_id/templates.ts
+++ b/src/api/routes/guilds/#guild_id/templates.ts
@@ -16,11 +16,10 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { generateCode, route } from "@spacebar/api";
 import { Guild, Template } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
-import { generateCode } from "@spacebar/api";
 
 const router: Router = Router();
 
@@ -41,19 +40,46 @@ const TemplateGuildProjection: (keyof Guild)[] = [
 	"icon",
 ];
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APITemplateArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	const templates = await Template.find({
-		where: { source_guild_id: guild_id },
-	});
+		const templates = await Template.find({
+			where: { source_guild_id: guild_id },
+		});
 
-	return res.json(templates);
-});
+		return res.json(templates);
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "TemplateCreateSchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "Template",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const guild = await Guild.findOneOrFail({
@@ -81,7 +107,13 @@ router.post(
 
 router.delete(
 	"/:code",
-	route({ permission: "MANAGE_GUILD" }),
+	route({
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: { body: "Template" },
+			403: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { code, guild_id } = req.params;
 
@@ -96,7 +128,13 @@ router.delete(
 
 router.put(
 	"/:code",
-	route({ permission: "MANAGE_GUILD" }),
+	route({
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: { body: "Template" },
+			403: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { code, guild_id } = req.params;
 		const guild = await Guild.findOneOrFail({
@@ -115,7 +153,14 @@ router.put(
 
 router.patch(
 	"/:code",
-	route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "TemplateModifySchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: { body: "Template" },
+			403: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { code, guild_id } = req.params;
 		const { name, description } = req.body;
diff --git a/src/api/routes/guilds/#guild_id/vanity-url.ts b/src/api/routes/guilds/#guild_id/vanity-url.ts
index c85c943f..a64ae2c9 100644
--- a/src/api/routes/guilds/#guild_id/vanity-url.ts
+++ b/src/api/routes/guilds/#guild_id/vanity-url.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelType,
@@ -23,8 +24,7 @@ import {
 	Invite,
 	VanityUrlSchema,
 } from "@spacebar/util";
-import { Router, Request, Response } from "express";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 const router = Router();
@@ -33,7 +33,20 @@ const InviteRegex = /\W/g;
 
 router.get(
 	"/",
-	route({ permission: "MANAGE_GUILD" }),
+	route({
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "GuildVanityUrlResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
@@ -60,7 +73,21 @@ router.get(
 
 router.patch(
 	"/",
-	route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "VanityUrlSchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "GuildVanityUrlCreateResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const body = req.body as VanityUrlSchema;
@@ -80,6 +107,17 @@ router.patch(
 			where: { guild_id, type: ChannelType.GUILD_TEXT },
 		});
 
+		if (!guild.features.includes("ALIASABLE_NAMES")) {
+			await Invite.update(
+				{ guild_id },
+				{
+					code: code,
+				},
+			);
+
+			return res.json({ code });
+		}
+
 		await Invite.create({
 			vanity_url: true,
 			code: code,
diff --git a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
index 791ac102..60c69075 100644
--- a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelType,
@@ -26,7 +27,6 @@ import {
 	VoiceStateUpdateEvent,
 	VoiceStateUpdateSchema,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
 import { Request, Response, Router } from "express";
 
 const router = Router();
@@ -34,7 +34,21 @@ const router = Router();
 
 router.patch(
 	"/",
-	route({ body: "VoiceStateUpdateSchema" }),
+	route({
+		requestBody: "VoiceStateUpdateSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as VoiceStateUpdateSchema;
 		const { guild_id } = req.params;
diff --git a/src/api/routes/guilds/#guild_id/welcome-screen.ts b/src/api/routes/guilds/#guild_id/welcome-screen.ts
index 696e20db..81000b4b 100644
--- a/src/api/routes/guilds/#guild_id/welcome-screen.ts
+++ b/src/api/routes/guilds/#guild_id/welcome-screen.ts
@@ -16,27 +16,53 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
-import { Guild, Member, GuildUpdateWelcomeScreenSchema } from "@spacebar/util";
-import { HTTPError } from "lambert-server";
 import { route } from "@spacebar/api";
+import {
+	Channel,
+	Guild,
+	GuildUpdateWelcomeScreenSchema,
+	Member,
+} from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const guild_id = req.params.guild_id;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildWelcomeScreen",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const guild_id = req.params.guild_id;
 
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	res.json(guild.welcome_screen);
-});
+		res.json(guild.welcome_screen);
+	},
+);
 
 router.patch(
 	"/",
 	route({
-		body: "GuildUpdateWelcomeScreenSchema",
+		requestBody: "GuildUpdateWelcomeScreenSchema",
 		permission: "MANAGE_GUILD",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const guild_id = req.params.guild_id;
@@ -44,17 +70,28 @@ router.patch(
 
 		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
 
-		if (!guild.welcome_screen.enabled)
-			throw new HTTPError("Welcome screen disabled", 400);
-		if (body.welcome_channels)
-			guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid
-		if (body.description)
+		if (body.enabled != undefined)
+			guild.welcome_screen.enabled = body.enabled;
+
+		if (body.description != undefined)
 			guild.welcome_screen.description = body.description;
-		if (body.enabled != null) guild.welcome_screen.enabled = body.enabled;
+
+		if (body.welcome_channels != undefined) {
+			// Ensure channels exist within the guild
+			await Promise.all(
+				body.welcome_channels?.map(({ channel_id }) =>
+					Channel.findOneOrFail({
+						where: { id: channel_id, guild_id },
+						select: { id: true },
+					}),
+				) || [],
+			);
+			guild.welcome_screen.welcome_channels = body.welcome_channels;
+		}
 
 		await guild.save();
 
-		res.sendStatus(204);
+		res.status(200).json(guild.welcome_screen);
 	},
 );
 
diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts
index 1799f0be..69b5d48c 100644
--- a/src/api/routes/guilds/#guild_id/widget.json.ts
+++ b/src/api/routes/guilds/#guild_id/widget.json.ts
@@ -16,10 +16,10 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { random, route } from "@spacebar/api";
+import { Channel, Guild, Invite, Member, Permissions } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { Permissions, Guild, Invite, Channel, Member } from "@spacebar/util";
 import { HTTPError } from "lambert-server";
-import { random, route } from "@spacebar/api";
 
 const router: Router = Router();
 
@@ -32,77 +32,90 @@ const router: Router = Router();
 
 // https://discord.com/developers/docs/resources/guild#get-guild-widget
 // TODO: Cache the response for a guild for 5 minutes regardless of response
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildWidgetJsonResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
-	if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
 
-	// Fetch existing widget invite for widget channel
-	let invite = await Invite.findOne({
-		where: { channel_id: guild.widget_channel_id },
-	});
+		// Fetch existing widget invite for widget channel
+		let invite = await Invite.findOne({
+			where: { channel_id: guild.widget_channel_id },
+		});
 
-	if (guild.widget_channel_id && !invite) {
-		// Create invite for channel if none exists
-		// TODO: Refactor invite create code to a shared function
-		const max_age = 86400; // 24 hours
-		const expires_at = new Date(max_age * 1000 + Date.now());
+		if (guild.widget_channel_id && !invite) {
+			// Create invite for channel if none exists
+			// TODO: Refactor invite create code to a shared function
+			const max_age = 86400; // 24 hours
+			const expires_at = new Date(max_age * 1000 + Date.now());
 
-		invite = await Invite.create({
-			code: random(),
-			temporary: false,
-			uses: 0,
-			max_uses: 0,
-			max_age: max_age,
-			expires_at,
-			created_at: new Date(),
-			guild_id,
-			channel_id: guild.widget_channel_id,
-		}).save();
-	}
+			invite = await Invite.create({
+				code: random(),
+				temporary: false,
+				uses: 0,
+				max_uses: 0,
+				max_age: max_age,
+				expires_at,
+				created_at: new Date(),
+				guild_id,
+				channel_id: guild.widget_channel_id,
+			}).save();
+		}
 
-	// Fetch voice channels, and the @everyone permissions object
-	const channels: { id: string; name: string; position: number }[] = [];
+		// Fetch voice channels, and the @everyone permissions object
+		const channels: { id: string; name: string; position: number }[] = [];
 
-	(
-		await Channel.find({
-			where: { guild_id: guild_id, type: 2 },
-			order: { position: "ASC" },
-		})
-	).filter((doc) => {
-		// Only return channels where @everyone has the CONNECT permission
-		if (
-			doc.permission_overwrites === undefined ||
-			Permissions.channelPermission(
-				doc.permission_overwrites,
-				Permissions.FLAGS.CONNECT,
-			) === Permissions.FLAGS.CONNECT
-		) {
-			channels.push({
-				id: doc.id,
-				name: doc.name ?? "Unknown channel",
-				position: doc.position ?? 0,
-			});
-		}
-	});
+		(
+			await Channel.find({
+				where: { guild_id: guild_id, type: 2 },
+				order: { position: "ASC" },
+			})
+		).filter((doc) => {
+			// Only return channels where @everyone has the CONNECT permission
+			if (
+				doc.permission_overwrites === undefined ||
+				Permissions.channelPermission(
+					doc.permission_overwrites,
+					Permissions.FLAGS.CONNECT,
+				) === Permissions.FLAGS.CONNECT
+			) {
+				channels.push({
+					id: doc.id,
+					name: doc.name ?? "Unknown channel",
+					position: doc.position ?? 0,
+				});
+			}
+		});
 
-	// Fetch members
-	// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
-	const members = await Member.find({ where: { guild_id: guild_id } });
+		// Fetch members
+		// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
+		const members = await Member.find({ where: { guild_id: guild_id } });
 
-	// Construct object to respond with
-	const data = {
-		id: guild_id,
-		name: guild.name,
-		instant_invite: invite?.code,
-		channels: channels,
-		members: members,
-		presence_count: guild.presence_count,
-	};
+		// Construct object to respond with
+		const data = {
+			id: guild_id,
+			name: guild.name,
+			instant_invite: invite?.code,
+			channels: channels,
+			members: members,
+			presence_count: guild.presence_count,
+		};
 
-	res.set("Cache-Control", "public, max-age=300");
-	return res.json(data);
-});
+		res.set("Cache-Control", "public, max-age=300");
+		return res.json(data);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/widget.png.ts b/src/api/routes/guilds/#guild_id/widget.png.ts
index 4e975603..c9ba8afc 100644
--- a/src/api/routes/guilds/#guild_id/widget.png.ts
+++ b/src/api/routes/guilds/#guild_id/widget.png.ts
@@ -18,11 +18,11 @@
 
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
-import { Request, Response, Router } from "express";
-import { Guild } from "@spacebar/util";
-import { HTTPError } from "lambert-server";
 import { route } from "@spacebar/api";
+import { Guild } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import fs from "fs";
+import { HTTPError } from "lambert-server";
 import path from "path";
 
 const router: Router = Router();
@@ -31,130 +31,178 @@ const router: Router = Router();
 
 // https://discord.com/developers/docs/resources/guild#get-guild-widget-image
 // TODO: Cache the response
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
-	if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
-
-	// Fetch guild information
-	const icon = guild.icon;
-	const name = guild.name;
-	const presence = guild.presence_count + " ONLINE";
-
-	// Fetch parameter
-	const style = req.query.style?.toString() || "shield";
-	if (
-		!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)
-	) {
-		throw new HTTPError(
-			"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
-			400,
-		);
-	}
-
-	// Setup canvas
-	const { createCanvas } = require("canvas");
-	const { loadImage } = require("canvas");
-	const sizeOf = require("image-size");
-
-	// TODO: Widget style templates need Spacebar branding
-	const source = path.join(
-		__dirname,
-		"..",
-		"..",
-		"..",
-		"..",
-		"..",
-		"assets",
-		"widget",
-		`${style}.png`,
-	);
-	if (!fs.existsSync(source)) {
-		throw new HTTPError("Widget template does not exist.", 400);
-	}
-
-	// Create base template image for parameter
-	const { width, height } = await sizeOf(source);
-	const canvas = createCanvas(width, height);
-	const ctx = canvas.getContext("2d");
-	const template = await loadImage(source);
-	ctx.drawImage(template, 0, 0);
-
-	// Add the guild specific information to the template asset image
-	switch (style) {
-		case "shield":
-			ctx.textAlign = "center";
-			await drawText(
-				ctx,
-				73,
-				13,
-				"#FFFFFF",
-				"thin 10px Verdana",
-				presence,
-			);
-			break;
-		case "banner1":
-			if (icon) await drawIcon(ctx, 20, 27, 50, icon);
-			await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22);
-			await drawText(
-				ctx,
-				83,
-				66,
-				"#C9D2F0FF",
-				"thin 11px Verdana",
-				presence,
-			);
-			break;
-		case "banner2":
-			if (icon) await drawIcon(ctx, 13, 19, 36, icon);
-			await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15);
-			await drawText(
-				ctx,
-				62,
-				49,
-				"#C9D2F0FF",
-				"thin 11px Verdana",
-				presence,
-			);
-			break;
-		case "banner3":
-			if (icon) await drawIcon(ctx, 20, 20, 50, icon);
-			await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27);
-			await drawText(
-				ctx,
-				83,
-				58,
-				"#C9D2F0FF",
-				"thin 11px Verdana",
-				presence,
-			);
-			break;
-		case "banner4":
-			if (icon) await drawIcon(ctx, 21, 136, 50, icon);
-			await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27);
-			await drawText(
-				ctx,
-				84,
-				171,
-				"#C9D2F0FF",
-				"thin 12px Verdana",
-				presence,
-			);
-			break;
-		default:
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
+
+		// Fetch guild information
+		const icon = guild.icon;
+		const name = guild.name;
+		const presence = guild.presence_count + " ONLINE";
+
+		// Fetch parameter
+		const style = req.query.style?.toString() || "shield";
+		if (
+			!["shield", "banner1", "banner2", "banner3", "banner4"].includes(
+				style,
+			)
+		) {
 			throw new HTTPError(
 				"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
 				400,
 			);
-	}
-
-	// Return final image
-	const buffer = canvas.toBuffer("image/png");
-	res.set("Content-Type", "image/png");
-	res.set("Cache-Control", "public, max-age=3600");
-	return res.send(buffer);
-});
+		}
+
+		// Setup canvas
+		const { createCanvas } = require("canvas");
+		const { loadImage } = require("canvas");
+		const sizeOf = require("image-size");
+
+		// TODO: Widget style templates need Spacebar branding
+		const source = path.join(
+			__dirname,
+			"..",
+			"..",
+			"..",
+			"..",
+			"..",
+			"assets",
+			"widget",
+			`${style}.png`,
+		);
+		if (!fs.existsSync(source)) {
+			throw new HTTPError("Widget template does not exist.", 400);
+		}
+
+		// Create base template image for parameter
+		const { width, height } = await sizeOf(source);
+		const canvas = createCanvas(width, height);
+		const ctx = canvas.getContext("2d");
+		const template = await loadImage(source);
+		ctx.drawImage(template, 0, 0);
+
+		// Add the guild specific information to the template asset image
+		switch (style) {
+			case "shield":
+				ctx.textAlign = "center";
+				await drawText(
+					ctx,
+					73,
+					13,
+					"#FFFFFF",
+					"thin 10px Verdana",
+					presence,
+				);
+				break;
+			case "banner1":
+				if (icon) await drawIcon(ctx, 20, 27, 50, icon);
+				await drawText(
+					ctx,
+					83,
+					51,
+					"#FFFFFF",
+					"12px Verdana",
+					name,
+					22,
+				);
+				await drawText(
+					ctx,
+					83,
+					66,
+					"#C9D2F0FF",
+					"thin 11px Verdana",
+					presence,
+				);
+				break;
+			case "banner2":
+				if (icon) await drawIcon(ctx, 13, 19, 36, icon);
+				await drawText(
+					ctx,
+					62,
+					34,
+					"#FFFFFF",
+					"12px Verdana",
+					name,
+					15,
+				);
+				await drawText(
+					ctx,
+					62,
+					49,
+					"#C9D2F0FF",
+					"thin 11px Verdana",
+					presence,
+				);
+				break;
+			case "banner3":
+				if (icon) await drawIcon(ctx, 20, 20, 50, icon);
+				await drawText(
+					ctx,
+					83,
+					44,
+					"#FFFFFF",
+					"12px Verdana",
+					name,
+					27,
+				);
+				await drawText(
+					ctx,
+					83,
+					58,
+					"#C9D2F0FF",
+					"thin 11px Verdana",
+					presence,
+				);
+				break;
+			case "banner4":
+				if (icon) await drawIcon(ctx, 21, 136, 50, icon);
+				await drawText(
+					ctx,
+					84,
+					156,
+					"#FFFFFF",
+					"13px Verdana",
+					name,
+					27,
+				);
+				await drawText(
+					ctx,
+					84,
+					171,
+					"#C9D2F0FF",
+					"thin 12px Verdana",
+					presence,
+				);
+				break;
+			default:
+				throw new HTTPError(
+					"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
+					400,
+				);
+		}
+
+		// Return final image
+		const buffer = canvas.toBuffer("image/png");
+		res.set("Content-Type", "image/png");
+		res.set("Cache-Control", "public, max-age=3600");
+		return res.send(buffer);
+	},
+);
 
 async function drawIcon(
 	canvas: any,
diff --git a/src/api/routes/guilds/#guild_id/widget.ts b/src/api/routes/guilds/#guild_id/widget.ts
index 77af25dc..cae0d6be 100644
--- a/src/api/routes/guilds/#guild_id/widget.ts
+++ b/src/api/routes/guilds/#guild_id/widget.ts
@@ -16,28 +16,55 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
-import { Guild, WidgetModifySchema } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { Guild, WidgetModifySchema } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 // https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildWidgetSettingsResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
 
-	return res.json({
-		enabled: guild.widget_enabled || false,
-		channel_id: guild.widget_channel_id || null,
-	});
-});
+		return res.json({
+			enabled: guild.widget_enabled || false,
+			channel_id: guild.widget_channel_id || null,
+		});
+	},
+);
 
 // https://discord.com/developers/docs/resources/guild#modify-guild-widget
 router.patch(
 	"/",
-	route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "WidgetModifySchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "WidgetModifySchema",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as WidgetModifySchema;
 		const { guild_id } = req.params;
diff --git a/src/api/routes/guilds/index.ts b/src/api/routes/guilds/index.ts
index c793d185..545beb18 100644
--- a/src/api/routes/guilds/index.ts
+++ b/src/api/routes/guilds/index.ts
@@ -16,16 +16,16 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
-	Guild,
 	Config,
-	getRights,
-	Member,
 	DiscordApiErrors,
+	Guild,
 	GuildCreateSchema,
+	Member,
+	getRights,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
@@ -33,7 +33,21 @@ const router: Router = Router();
 
 router.post(
 	"/",
-	route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }),
+	route({
+		requestBody: "GuildCreateSchema",
+		right: "CREATE_GUILDS",
+		responses: {
+			201: {
+				body: "GuildCreateResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as GuildCreateSchema;
 
@@ -58,7 +72,7 @@ router.post(
 
 		await Member.addToGuild(req.user_id, guild.id);
 
-		res.status(201).json({ id: guild.id });
+		res.status(201).json(guild);
 	},
 );
 
diff --git a/src/api/routes/guilds/templates/index.ts b/src/api/routes/guilds/templates/index.ts
index bfbb7d3b..8f718a21 100644
--- a/src/api/routes/guilds/templates/index.ts
+++ b/src/api/routes/guilds/templates/index.ts
@@ -16,72 +16,91 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
-	Template,
+	Config,
+	DiscordApiErrors,
 	Guild,
+	GuildTemplateCreateSchema,
+	Member,
 	Role,
 	Snowflake,
-	Config,
-	Member,
-	GuildTemplateCreateSchema,
+	Template,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
-import { DiscordApiErrors } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import fetch from "node-fetch";
 const router: Router = Router();
 
-router.get("/:code", route({}), async (req: Request, res: Response) => {
-	const { allowDiscordTemplates, allowRaws, enabled } =
-		Config.get().templates;
-	if (!enabled)
-		res.json({
-			code: 403,
-			message: "Template creation & usage is disabled on this instance.",
-		}).sendStatus(403);
-
-	const { code } = req.params;
+router.get(
+	"/:code",
+	route({
+		responses: {
+			200: {
+				body: "Template",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { allowDiscordTemplates, allowRaws, enabled } =
+			Config.get().templates;
+		if (!enabled)
+			res.json({
+				code: 403,
+				message:
+					"Template creation & usage is disabled on this instance.",
+			}).sendStatus(403);
 
-	if (code.startsWith("discord:")) {
-		if (!allowDiscordTemplates)
-			return res
-				.json({
-					code: 403,
-					message:
-						"Discord templates cannot be used on this instance.",
-				})
-				.sendStatus(403);
-		const discordTemplateID = code.split("discord:", 2)[1];
+		const { code } = req.params;
 
-		const discordTemplateData = await fetch(
-			`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`,
-			{
-				method: "get",
-				headers: { "Content-Type": "application/json" },
-			},
-		);
-		return res.json(await discordTemplateData.json());
-	}
+		if (code.startsWith("discord:")) {
+			if (!allowDiscordTemplates)
+				return res
+					.json({
+						code: 403,
+						message:
+							"Discord templates cannot be used on this instance.",
+					})
+					.sendStatus(403);
+			const discordTemplateID = code.split("discord:", 2)[1];
+
+			const discordTemplateData = await fetch(
+				`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`,
+				{
+					method: "get",
+					headers: { "Content-Type": "application/json" },
+				},
+			);
+			return res.json(await discordTemplateData.json());
+		}
 
-	if (code.startsWith("external:")) {
-		if (!allowRaws)
-			return res
-				.json({
-					code: 403,
-					message: "Importing raws is disabled on this instance.",
-				})
-				.sendStatus(403);
+		if (code.startsWith("external:")) {
+			if (!allowRaws)
+				return res
+					.json({
+						code: 403,
+						message: "Importing raws is disabled on this instance.",
+					})
+					.sendStatus(403);
 
-		return res.json(code.split("external:", 2)[1]);
-	}
+			return res.json(code.split("external:", 2)[1]);
+		}
 
-	const template = await Template.findOneOrFail({ where: { code: code } });
-	res.json(template);
-});
+		const template = await Template.findOneOrFail({
+			where: { code: code },
+		});
+		res.json(template);
+	},
+);
 
 router.post(
 	"/:code",
-	route({ body: "GuildTemplateCreateSchema" }),
+	route({ requestBody: "GuildTemplateCreateSchema" }),
 	async (req: Request, res: Response) => {
 		const {
 			enabled,
diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts
index 8bff2200..28a3b429 100644
--- a/src/api/routes/invites/index.ts
+++ b/src/api/routes/invites/index.ts
@@ -16,36 +16,68 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
+	DiscordApiErrors,
 	emitEvent,
 	getPermission,
 	Guild,
 	Invite,
 	InviteDeleteEvent,
-	User,
 	PublicInviteRelation,
+	User,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 const router: Router = Router();
 
-router.get("/:code", route({}), async (req: Request, res: Response) => {
-	const { code } = req.params;
+router.get(
+	"/:code",
+	route({
+		responses: {
+			"200": {
+				body: "Invite",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { code } = req.params;
 
-	const invite = await Invite.findOneOrFail({
-		where: { code },
-		relations: PublicInviteRelation,
-	});
+		const invite = await Invite.findOneOrFail({
+			where: { code },
+			relations: PublicInviteRelation,
+		});
 
-	res.status(200).send(invite);
-});
+		res.status(200).send(invite);
+	},
+);
 
 router.post(
 	"/:code",
-	route({ right: "USE_MASS_INVITES" }),
+	route({
+		right: "USE_MASS_INVITES",
+		responses: {
+			"200": {
+				body: "Invite",
+			},
+			401: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
+		if (req.user_bot) throw DiscordApiErrors.BOT_PROHIBITED_ENDPOINT;
+
 		const { code } = req.params;
 		const { guild_id } = await Invite.findOneOrFail({
 			where: { code: code },
@@ -75,33 +107,56 @@ router.post(
 );
 
 // * cant use permission of route() function because path doesn't have guild_id/channel_id
-router.delete("/:code", route({}), async (req: Request, res: Response) => {
-	const { code } = req.params;
-	const invite = await Invite.findOneOrFail({ where: { code } });
-	const { guild_id, channel_id } = invite;
-
-	const permission = await getPermission(req.user_id, guild_id, channel_id);
+router.delete(
+	"/:code",
+	route({
+		responses: {
+			"200": {
+				body: "Invite",
+			},
+			401: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { code } = req.params;
+		const invite = await Invite.findOneOrFail({ where: { code } });
+		const { guild_id, channel_id } = invite;
 
-	if (!permission.has("MANAGE_GUILD") && !permission.has("MANAGE_CHANNELS"))
-		throw new HTTPError(
-			"You missing the MANAGE_GUILD or MANAGE_CHANNELS permission",
-			401,
+		const permission = await getPermission(
+			req.user_id,
+			guild_id,
+			channel_id,
 		);
 
-	await Promise.all([
-		Invite.delete({ code }),
-		emitEvent({
-			event: "INVITE_DELETE",
-			guild_id: guild_id,
-			data: {
-				channel_id: channel_id,
+		if (
+			!permission.has("MANAGE_GUILD") &&
+			!permission.has("MANAGE_CHANNELS")
+		)
+			throw new HTTPError(
+				"You missing the MANAGE_GUILD or MANAGE_CHANNELS permission",
+				401,
+			);
+
+		await Promise.all([
+			Invite.delete({ code }),
+			emitEvent({
+				event: "INVITE_DELETE",
 				guild_id: guild_id,
-				code: code,
-			},
-		} as InviteDeleteEvent),
-	]);
+				data: {
+					channel_id: channel_id,
+					guild_id: guild_id,
+					code: code,
+				},
+			} as InviteDeleteEvent),
+		]);
 
-	res.json({ invite: invite });
-});
+		res.json({ invite: invite });
+	},
+);
 
 export default router;
diff --git a/src/api/routes/oauth2/authorize.ts b/src/api/routes/oauth2/authorize.ts
index c041f671..7ae6fa84 100644
--- a/src/api/routes/oauth2/authorize.ts
+++ b/src/api/routes/oauth2/authorize.ts
@@ -16,126 +16,168 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import {
 	ApiError,
 	Application,
 	ApplicationAuthorizeSchema,
-	getPermission,
 	DiscordApiErrors,
 	Member,
 	Permissions,
 	User,
+	getPermission,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
 // TODO: scopes, other oauth types
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// const { client_id, scope, response_type, redirect_url } = req.query;
-	const { client_id } = req.query;
-
-	const app = await Application.findOne({
-		where: {
-			id: client_id as string,
+router.get(
+	"/",
+	route({
+		responses: {
+			// TODO: I really didn't feel like typing all of it out
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
 		},
-		relations: ["bot"],
-	});
+	}),
+	async (req: Request, res: Response) => {
+		// const { client_id, scope, response_type, redirect_url } = req.query;
+		const { client_id } = req.query;
 
-	// TODO: use DiscordApiErrors
-	// findOneOrFail throws code 404
-	if (!app) throw DiscordApiErrors.UNKNOWN_APPLICATION;
-	if (!app.bot) throw DiscordApiErrors.OAUTH2_APPLICATION_BOT_ABSENT;
+		const app = await Application.findOne({
+			where: {
+				id: client_id as string,
+			},
+			relations: ["bot"],
+		});
 
-	const bot = app.bot;
-	delete app.bot;
+		// TODO: use DiscordApiErrors
+		// findOneOrFail throws code 404
+		if (!app) throw DiscordApiErrors.UNKNOWN_APPLICATION;
+		if (!app.bot) throw DiscordApiErrors.OAUTH2_APPLICATION_BOT_ABSENT;
 
-	const user = await User.findOneOrFail({
-		where: {
-			id: req.user_id,
-			bot: false,
-		},
-		select: ["id", "username", "avatar", "discriminator", "public_flags"],
-	});
+		const bot = app.bot;
+		delete app.bot;
 
-	const guilds = await Member.find({
-		where: {
-			user: {
+		const user = await User.findOneOrFail({
+			where: {
 				id: req.user_id,
+				bot: false,
 			},
-		},
-		relations: ["guild", "roles"],
-		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-		//@ts-ignore
-		// prettier-ignore
-		select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id"],
-	});
-
-	const guildsWithPermissions = guilds.map((x) => {
-		const perms =
-			x.guild.owner_id === user.id
-				? new Permissions(Permissions.FLAGS.ADMINISTRATOR)
-				: Permissions.finalPermission({
-						user: {
-							id: user.id,
-							roles: x.roles?.map((x) => x.id) || [],
-						},
-						guild: {
-							roles: x?.roles || [],
-						},
-				  });
-
-		return {
-			id: x.guild.id,
-			name: x.guild.name,
-			icon: x.guild.icon,
-			mfa_level: x.guild.mfa_level,
-			permissions: perms.bitfield.toString(),
-		};
-	});
-
-	return res.json({
-		guilds: guildsWithPermissions,
-		user: {
-			id: user.id,
-			username: user.username,
-			avatar: user.avatar,
-			avatar_decoration: null, // TODO
-			discriminator: user.discriminator,
-			public_flags: user.public_flags,
-		},
-		application: {
-			id: app.id,
-			name: app.name,
-			icon: app.icon,
-			description: app.description,
-			summary: app.summary,
-			type: app.type,
-			hook: app.hook,
-			guild_id: null, // TODO support guilds
-			bot_public: app.bot_public,
-			bot_require_code_grant: app.bot_require_code_grant,
-			verify_key: app.verify_key,
-			flags: app.flags,
-		},
-		bot: {
-			id: bot.id,
-			username: bot.username,
-			avatar: bot.avatar,
-			avatar_decoration: null, // TODO
-			discriminator: bot.discriminator,
-			public_flags: bot.public_flags,
-			bot: true,
-			approximated_guild_count: 0, // TODO
-		},
-		authorized: false,
-	});
-});
+			select: [
+				"id",
+				"username",
+				"avatar",
+				"discriminator",
+				"public_flags",
+			],
+		});
+
+		const guilds = await Member.find({
+			where: {
+				user: {
+					id: req.user_id,
+				},
+			},
+			relations: ["guild", "roles"],
+			// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+			//@ts-ignore
+			// prettier-ignore
+			select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id"],
+		});
+
+		const guildsWithPermissions = guilds.map((x) => {
+			const perms =
+				x.guild.owner_id === user.id
+					? new Permissions(Permissions.FLAGS.ADMINISTRATOR)
+					: Permissions.finalPermission({
+							user: {
+								id: user.id,
+								roles: x.roles?.map((x) => x.id) || [],
+							},
+							guild: {
+								roles: x?.roles || [],
+							},
+					  });
+
+			return {
+				id: x.guild.id,
+				name: x.guild.name,
+				icon: x.guild.icon,
+				mfa_level: x.guild.mfa_level,
+				permissions: perms.bitfield.toString(),
+			};
+		});
+
+		return res.json({
+			guilds: guildsWithPermissions,
+			user: {
+				id: user.id,
+				username: user.username,
+				avatar: user.avatar,
+				avatar_decoration: null, // TODO
+				discriminator: user.discriminator,
+				public_flags: user.public_flags,
+			},
+			application: {
+				id: app.id,
+				name: app.name,
+				icon: app.icon,
+				description: app.description,
+				summary: app.summary,
+				type: app.type,
+				hook: app.hook,
+				guild_id: null, // TODO support guilds
+				bot_public: app.bot_public,
+				bot_require_code_grant: app.bot_require_code_grant,
+				verify_key: app.verify_key,
+				flags: app.flags,
+			},
+			bot: {
+				id: bot.id,
+				username: bot.username,
+				avatar: bot.avatar,
+				avatar_decoration: null, // TODO
+				discriminator: bot.discriminator,
+				public_flags: bot.public_flags,
+				bot: true,
+				approximated_guild_count: 0, // TODO
+			},
+			authorized: false,
+		});
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "ApplicationAuthorizeSchema" }),
+	route({
+		requestBody: "ApplicationAuthorizeSchema",
+		query: {
+			client_id: {
+				type: "string",
+			},
+		},
+		responses: {
+			200: {
+				body: "OAuthAuthorizeResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as ApplicationAuthorizeSchema;
 		// const { client_id, scope, response_type, redirect_url } = req.query;
diff --git a/src/api/routes/ping.ts b/src/api/routes/ping.ts
index 0fb6d9d0..73330239 100644
--- a/src/api/routes/ping.ts
+++ b/src/api/routes/ping.ts
@@ -16,29 +16,39 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), (req: Request, res: Response) => {
-	const { general } = Config.get();
-	res.send({
-		ping: "pong!",
-		instance: {
-			id: general.instanceId,
-			name: general.instanceName,
-			description: general.instanceDescription,
-			image: general.image,
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "InstancePingResponse",
+			},
+		},
+	}),
+	(req: Request, res: Response) => {
+		const { general } = Config.get();
+		res.send({
+			ping: "pong!",
+			instance: {
+				id: general.instanceId,
+				name: general.instanceName,
+				description: general.instanceDescription,
+				image: general.image,
 
-			correspondenceEmail: general.correspondenceEmail,
-			correspondenceUserID: general.correspondenceUserID,
+				correspondenceEmail: general.correspondenceEmail,
+				correspondenceUserID: general.correspondenceUserID,
 
-			frontPage: general.frontPage,
-			tosPage: general.tosPage,
-		},
-	});
-});
+				frontPage: general.frontPage,
+				tosPage: general.tosPage,
+			},
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/policies/instance/domains.ts b/src/api/routes/policies/instance/domains.ts
index 696a8510..afeb0e85 100644
--- a/src/api/routes/policies/instance/domains.ts
+++ b/src/api/routes/policies/instance/domains.ts
@@ -16,25 +16,38 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { cdn, gateway, api } = Config.get();
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "InstanceDomainsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { cdn, gateway, api } = Config.get();
 
-	const IdentityForm = {
-		cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001",
-		gateway:
-			gateway.endpointPublic ||
-			process.env.GATEWAY ||
-			"ws://localhost:3001",
-		defaultApiVersion: api.defaultVersion ?? 9,
-		apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/",
-	};
+		const IdentityForm = {
+			cdn:
+				cdn.endpointPublic ||
+				process.env.CDN ||
+				"http://localhost:3001",
+			gateway:
+				gateway.endpointPublic ||
+				process.env.GATEWAY ||
+				"ws://localhost:3001",
+			defaultApiVersion: api.defaultVersion ?? 9,
+			apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/",
+		};
 
-	res.json(IdentityForm);
-});
+		res.json(IdentityForm);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/policies/instance/index.ts b/src/api/routes/policies/instance/index.ts
index 68ce3b42..6e269a5c 100644
--- a/src/api/routes/policies/instance/index.ts
+++ b/src/api/routes/policies/instance/index.ts
@@ -16,14 +16,24 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { general } = Config.get();
-	res.json(general);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIGeneralConfiguration",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { general } = Config.get();
+		res.json(general);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/policies/instance/limits.ts b/src/api/routes/policies/instance/limits.ts
index a6f13170..9852459d 100644
--- a/src/api/routes/policies/instance/limits.ts
+++ b/src/api/routes/policies/instance/limits.ts
@@ -16,14 +16,24 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { limits } = Config.get();
-	res.json(limits);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APILimitsConfiguration",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { limits } = Config.get();
+		res.json(limits);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/policies/stats.ts b/src/api/routes/policies/stats.ts
index 3939e1e8..b2cd3d5a 100644
--- a/src/api/routes/policies/stats.ts
+++ b/src/api/routes/policies/stats.ts
@@ -28,20 +28,33 @@ import {
 import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	if (!Config.get().security.statsWorldReadable) {
-		const rights = await getRights(req.user_id);
-		rights.hasThrow("VIEW_SERVER_STATS");
-	}
-
-	res.json({
-		counts: {
-			user: await User.count(),
-			guild: await Guild.count(),
-			message: await Message.count(),
-			members: await Member.count(),
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "InstanceStatsResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
 		},
-	});
-});
+	}),
+	async (req: Request, res: Response) => {
+		if (!Config.get().security.statsWorldReadable) {
+			const rights = await getRights(req.user_id);
+			rights.hasThrow("VIEW_SERVER_STATS");
+		}
+
+		res.json({
+			counts: {
+				user: await User.count(),
+				guild: await Guild.count(),
+				message: await Message.count(),
+				members: await Member.count(),
+			},
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/read-states/ack-bulk.ts b/src/api/routes/read-states/ack-bulk.ts
index 2c51893b..3ee25d1a 100644
--- a/src/api/routes/read-states/ack-bulk.ts
+++ b/src/api/routes/read-states/ack-bulk.ts
@@ -16,14 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import { AckBulkSchema, ReadState } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "AckBulkSchema" }),
+	route({
+		requestBody: "AckBulkSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as AckBulkSchema;
 
diff --git a/src/api/routes/science.ts b/src/api/routes/science.ts
index 099da18b..d5cdc173 100644
--- a/src/api/routes/science.ts
+++ b/src/api/routes/science.ts
@@ -16,14 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.post("/", route({}), (req: Request, res: Response) => {
-	// TODO:
-	res.sendStatus(204);
-});
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+		},
+	}),
+	(req: Request, res: Response) => {
+		// TODO:
+		res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/sticker-packs/index.ts b/src/api/routes/sticker-packs/index.ts
index 234e03c6..569d1104 100644
--- a/src/api/routes/sticker-packs/index.ts
+++ b/src/api/routes/sticker-packs/index.ts
@@ -16,16 +16,28 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
 import { StickerPack } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const sticker_packs = await StickerPack.find({ relations: ["stickers"] });
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIStickerPackArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const sticker_packs = await StickerPack.find({
+			relations: ["stickers"],
+		});
 
-	res.json({ sticker_packs });
-});
+		res.json({ sticker_packs });
+	},
+);
 
 export default router;
diff --git a/src/api/routes/stickers/#sticker_id/index.ts b/src/api/routes/stickers/#sticker_id/index.ts
index 360149b5..2ea81bf9 100644
--- a/src/api/routes/stickers/#sticker_id/index.ts
+++ b/src/api/routes/stickers/#sticker_id/index.ts
@@ -16,15 +16,25 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Sticker } from "@spacebar/util";
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Sticker } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { sticker_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "Sticker",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { sticker_id } = req.params;
 
-	res.json(await Sticker.find({ where: { id: sticker_id } }));
-});
+		res.json(await Sticker.find({ where: { id: sticker_id } }));
+	},
+);
 
 export default router;
diff --git a/src/api/routes/stop.ts b/src/api/routes/stop.ts
index 6a6e6277..79e132d7 100644
--- a/src/api/routes/stop.ts
+++ b/src/api/routes/stop.ts
@@ -16,14 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.post(
 	"/",
-	route({ right: "OPERATOR" }),
+	route({
+		right: "OPERATOR",
+		responses: {
+			200: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		console.log(`/stop was called by ${req.user_id} at ${new Date()}`);
 		res.sendStatus(200);
diff --git a/src/api/routes/updates.ts b/src/api/routes/updates.ts
index f7403899..101bd3bc 100644
--- a/src/api/routes/updates.ts
+++ b/src/api/routes/updates.ts
@@ -16,37 +16,53 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
 import { FieldErrors, Release } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const platform = req.query.platform;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "UpdatesResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const platform = req.query.platform;
 
-	if (!platform)
-		throw FieldErrors({
-			platform: {
-				code: "BASE_TYPE_REQUIRED",
-				message: req.t("common:field.BASE_TYPE_REQUIRED"),
+		if (!platform)
+			throw FieldErrors({
+				platform: {
+					code: "BASE_TYPE_REQUIRED",
+					message: req.t("common:field.BASE_TYPE_REQUIRED"),
+				},
+			});
+
+		const release = await Release.findOneOrFail({
+			where: {
+				enabled: true,
+				platform: platform as string,
 			},
+			order: { pub_date: "DESC" },
 		});
 
-	const release = await Release.findOneOrFail({
-		where: {
-			enabled: true,
-			platform: platform as string,
-		},
-		order: { pub_date: "DESC" },
-	});
-
-	res.json({
-		name: release.name,
-		pub_date: release.pub_date,
-		url: release.url,
-		notes: release.notes,
-	});
-});
+		res.json({
+			name: release.name,
+			pub_date: release.pub_date,
+			url: release.url,
+			notes: release.notes,
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/#id/delete.ts b/src/api/routes/users/#id/delete.ts
index e36a35e6..5b1a682c 100644
--- a/src/api/routes/users/#id/delete.ts
+++ b/src/api/routes/users/#id/delete.ts
@@ -30,7 +30,18 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ right: "MANAGE_USERS" }),
+	route({
+		right: "MANAGE_USERS",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		await User.findOneOrFail({
 			where: { id: req.params.id },
diff --git a/src/api/routes/users/#id/index.ts b/src/api/routes/users/#id/index.ts
index 0c7cfe37..1bd413d3 100644
--- a/src/api/routes/users/#id/index.ts
+++ b/src/api/routes/users/#id/index.ts
@@ -16,16 +16,26 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { User } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { User } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIPublicUser",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { id } = req.params;
 
-	res.json(await User.getPublicUser(id));
-});
+		res.json(await User.getPublicUser(id));
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts
index 2836c563..eecec0f3 100644
--- a/src/api/routes/users/#id/profile.ts
+++ b/src/api/routes/users/#id/profile.ts
@@ -16,23 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
-	User,
 	Member,
-	UserProfileModifySchema,
-	handleFile,
 	PrivateUserProjection,
-	emitEvent,
+	User,
+	UserProfileModifySchema,
 	UserUpdateEvent,
+	emitEvent,
+	handleFile,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.get(
 	"/",
-	route({ test: { response: { body: "UserProfileResponse" } } }),
+	route({ responses: { 200: { body: "UserProfileResponse" } } }),
 	async (req: Request, res: Response) => {
 		if (req.params.id === "@me") req.params.id = req.user_id;
 
@@ -84,18 +84,6 @@ router.get(
 
 		// TODO: make proper DTO's in util?
 
-		const userDto = {
-			username: user.username,
-			discriminator: user.discriminator,
-			id: user.id,
-			public_flags: user.public_flags,
-			avatar: user.avatar,
-			accent_color: user.accent_color,
-			banner: user.banner,
-			bio: req.user_bot ? null : user.bio,
-			bot: user.bot,
-		};
-
 		const userProfile = {
 			bio: req.user_bot ? null : user.bio,
 			accent_color: user.accent_color,
@@ -104,28 +92,6 @@ router.get(
 			theme_colors: user.theme_colors,
 		};
 
-		const guildMemberDto = guild_member
-			? {
-					avatar: guild_member.avatar,
-					banner: guild_member.banner,
-					bio: req.user_bot ? null : guild_member.bio,
-					communication_disabled_until:
-						guild_member.communication_disabled_until,
-					deaf: guild_member.deaf,
-					flags: user.flags,
-					is_pending: guild_member.pending,
-					pending: guild_member.pending, // why is this here twice, discord?
-					joined_at: guild_member.joined_at,
-					mute: guild_member.mute,
-					nick: guild_member.nick,
-					premium_since: guild_member.premium_since,
-					roles: guild_member.roles
-						.map((x) => x.id)
-						.filter((id) => id != guild_id),
-					user: userDto,
-			  }
-			: undefined;
-
 		const guildMemberProfile = {
 			accent_color: null,
 			banner: guild_member?.banner || null,
@@ -139,11 +105,11 @@ router.get(
 			premium_guild_since: premium_guild_since, // TODO
 			premium_since: user.premium_since, // TODO
 			mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true
-			user: userDto,
+			user: user.toPublicUser(),
 			premium_type: user.premium_type,
 			profile_themes_experiment_bucket: 4, // TODO: This doesn't make it available, for some reason?
 			user_profile: userProfile,
-			guild_member: guild_id && guildMemberDto,
+			guild_member: guild_member?.toPublicMember(),
 			guild_member_profile: guild_id && guildMemberProfile,
 		});
 	},
@@ -151,7 +117,7 @@ router.get(
 
 router.patch(
 	"/",
-	route({ body: "UserProfileModifySchema" }),
+	route({ requestBody: "UserProfileModifySchema" }),
 	async (req: Request, res: Response) => {
 		const body = req.body as UserProfileModifySchema;
 
diff --git a/src/api/routes/users/#id/relationships.ts b/src/api/routes/users/#id/relationships.ts
index dfe52a5e..3737ca00 100644
--- a/src/api/routes/users/#id/relationships.ts
+++ b/src/api/routes/users/#id/relationships.ts
@@ -16,17 +16,25 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { User } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { User, UserRelationsResponse } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.get(
 	"/",
-	route({ test: { response: { body: "UserRelationsResponse" } } }),
+	route({
+		responses: {
+			200: { body: "UserRelationsResponse" },
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
-		const mutual_relations: object[] = [];
+		const mutual_relations: UserRelationsResponse = [];
+
 		const requested_relations = await User.findOneOrFail({
 			where: { id: req.params.id },
 			relations: ["relationships"],
diff --git a/src/api/routes/users/@me/channels.ts b/src/api/routes/users/@me/channels.ts
index 04db4fe9..8a8fadd9 100644
--- a/src/api/routes/users/@me/channels.ts
+++ b/src/api/routes/users/@me/channels.ts
@@ -16,32 +16,51 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
-	Recipient,
-	DmChannelDTO,
 	Channel,
 	DmChannelCreateSchema,
+	DmChannelDTO,
+	Recipient,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const recipients = await Recipient.find({
-		where: { user_id: req.user_id, closed: false },
-		relations: ["channel", "channel.recipients"],
-	});
-	res.json(
-		await Promise.all(
-			recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id])),
-		),
-	);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIDMChannelArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const recipients = await Recipient.find({
+			where: { user_id: req.user_id, closed: false },
+			relations: ["channel", "channel.recipients"],
+		});
+		res.json(
+			await Promise.all(
+				recipients.map((r) =>
+					DmChannelDTO.from(r.channel, [req.user_id]),
+				),
+			),
+		);
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "DmChannelCreateSchema" }),
+	route({
+		requestBody: "DmChannelCreateSchema",
+		responses: {
+			200: {
+				body: "DmChannelDTO",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as DmChannelCreateSchema;
 		res.json(
diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts
index 9031f3c8..789a7878 100644
--- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts
+++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts
@@ -23,9 +23,9 @@ import {
 	ConnectionStore,
 	DiscordApiErrors,
 	FieldErrors,
+	RefreshableConnection,
 } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import RefreshableConnection from "../../../../../../../util/connections/RefreshableConnection";
 const router = Router();
 
 // TODO: this route is only used for spotify, twitch, and youtube. (battlenet seems to be able to PUT, maybe others also)
diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts
index 3a4e5e0a..351ec99a 100644
--- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts
+++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts
@@ -29,7 +29,7 @@ const router = Router();
 // TODO: connection update schema
 router.patch(
 	"/",
-	route({ body: "ConnectionUpdateSchema" }),
+	route({ requestBody: "ConnectionUpdateSchema" }),
 	async (req: Request, res: Response) => {
 		const { connection_name, connection_id } = req.params;
 		const body = req.body as ConnectionUpdateSchema;
diff --git a/src/api/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts
index dce737fc..e36a1e92 100644
--- a/src/api/routes/users/@me/delete.ts
+++ b/src/api/routes/users/@me/delete.ts
@@ -16,41 +16,58 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { Member, User } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { Member, User } from "@spacebar/util";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 const router = Router();
 
-router.post("/", route({}), async (req: Request, res: Response) => {
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		select: ["data"],
-	}); //User object
-	let correctpass = true;
-
-	if (user.data.hash) {
-		// guest accounts can delete accounts without password
-		correctpass = await bcrypt.compare(req.body.password, user.data.hash);
-		if (!correctpass) {
-			throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+			401: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			select: ["data"],
+		}); //User object
+		let correctpass = true;
+
+		if (user.data.hash) {
+			// guest accounts can delete accounts without password
+			correctpass = await bcrypt.compare(
+				req.body.password,
+				user.data.hash,
+			);
+			if (!correctpass) {
+				throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
+			}
 		}
-	}
 
-	// TODO: decrement guild member count
+		// TODO: decrement guild member count
 
-	if (correctpass) {
-		await Promise.all([
-			User.delete({ id: req.user_id }),
-			Member.delete({ id: req.user_id }),
-		]);
+		if (correctpass) {
+			await Promise.all([
+				User.delete({ id: req.user_id }),
+				Member.delete({ id: req.user_id }),
+			]);
 
-		res.sendStatus(204);
-	} else {
-		res.sendStatus(401);
-	}
-});
+			res.sendStatus(204);
+		} else {
+			res.sendStatus(401);
+		}
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/disable.ts b/src/api/routes/users/@me/disable.ts
index d123a6a1..b4d03e62 100644
--- a/src/api/routes/users/@me/disable.ts
+++ b/src/api/routes/users/@me/disable.ts
@@ -16,35 +16,52 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { User } from "@spacebar/util";
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { User } from "@spacebar/util";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.post("/", route({}), async (req: Request, res: Response) => {
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		select: ["data"],
-	}); //User object
-	let correctpass = true;
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			select: ["data"],
+		}); //User object
+		let correctpass = true;
 
-	if (user.data.hash) {
-		// guest accounts can delete accounts without password
-		correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/
-	}
+		if (user.data.hash) {
+			// guest accounts can delete accounts without password
+			correctpass = await bcrypt.compare(
+				req.body.password,
+				user.data.hash,
+			); //Not sure if user typed right password :/
+		}
 
-	if (correctpass) {
-		await User.update({ id: req.user_id }, { disabled: true });
+		if (correctpass) {
+			await User.update({ id: req.user_id }, { disabled: true });
 
-		res.sendStatus(204);
-	} else {
-		res.status(400).json({
-			message: "Password does not match",
-			code: 50018,
-		});
-	}
-});
+			res.sendStatus(204);
+		} else {
+			res.status(400).json({
+				message: "Password does not match",
+				code: 50018,
+			});
+		}
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/guilds.ts b/src/api/routes/users/@me/guilds.ts
index b16b909d..0bce432b 100644
--- a/src/api/routes/users/@me/guilds.ts
+++ b/src/api/routes/users/@me/guilds.ts
@@ -16,79 +16,106 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
+	Config,
 	Guild,
-	Member,
-	User,
 	GuildDeleteEvent,
 	GuildMemberRemoveEvent,
+	Member,
+	User,
 	emitEvent,
-	Config,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const members = await Member.find({
-		relations: ["guild"],
-		where: { id: req.user_id },
-	});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIGuildArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const members = await Member.find({
+			relations: ["guild"],
+			where: { id: req.user_id },
+		});
 
-	let guild = members.map((x) => x.guild);
+		let guild = members.map((x) => x.guild);
 
-	if ("with_counts" in req.query && req.query.with_counts == "true") {
-		guild = []; // TODO: Load guilds with user role permissions number
-	}
+		if ("with_counts" in req.query && req.query.with_counts == "true") {
+			guild = []; // TODO: Load guilds with user role permissions number
+		}
 
-	res.json(guild);
-});
+		res.json(guild);
+	},
+);
 
 // user send to leave a certain guild
-router.delete("/:guild_id", route({}), async (req: Request, res: Response) => {
-	const { autoJoin } = Config.get().guild;
-	const { guild_id } = req.params;
-	const guild = await Guild.findOneOrFail({
-		where: { id: guild_id },
-		select: ["owner_id"],
-	});
+router.delete(
+	"/:guild_id",
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { autoJoin } = Config.get().guild;
+		const { guild_id } = req.params;
+		const guild = await Guild.findOneOrFail({
+			where: { id: guild_id },
+			select: ["owner_id"],
+		});
 
-	if (!guild) throw new HTTPError("Guild doesn't exist", 404);
-	if (guild.owner_id === req.user_id)
-		throw new HTTPError("You can't leave your own guild", 400);
-	if (
-		autoJoin.enabled &&
-		autoJoin.guilds.includes(guild_id) &&
-		!autoJoin.canLeave
-	) {
-		throw new HTTPError("You can't leave instance auto join guilds", 400);
-	}
+		if (!guild) throw new HTTPError("Guild doesn't exist", 404);
+		if (guild.owner_id === req.user_id)
+			throw new HTTPError("You can't leave your own guild", 400);
+		if (
+			autoJoin.enabled &&
+			autoJoin.guilds.includes(guild_id) &&
+			!autoJoin.canLeave
+		) {
+			throw new HTTPError(
+				"You can't leave instance auto join guilds",
+				400,
+			);
+		}
 
-	await Promise.all([
-		Member.delete({ id: req.user_id, guild_id: guild_id }),
-		emitEvent({
-			event: "GUILD_DELETE",
-			data: {
-				id: guild_id,
-			},
-			user_id: req.user_id,
-		} as GuildDeleteEvent),
-	]);
+		await Promise.all([
+			Member.delete({ id: req.user_id, guild_id: guild_id }),
+			emitEvent({
+				event: "GUILD_DELETE",
+				data: {
+					id: guild_id,
+				},
+				user_id: req.user_id,
+			} as GuildDeleteEvent),
+		]);
 
-	const user = await User.getPublicUser(req.user_id);
+		const user = await User.getPublicUser(req.user_id);
 
-	await emitEvent({
-		event: "GUILD_MEMBER_REMOVE",
-		data: {
+		await emitEvent({
+			event: "GUILD_MEMBER_REMOVE",
+			data: {
+				guild_id: guild_id,
+				user: user,
+			},
 			guild_id: guild_id,
-			user: user,
-		},
-		guild_id: guild_id,
-	} as GuildMemberRemoveEvent);
+		} as GuildMemberRemoveEvent);
 
-	return res.sendStatus(204);
-});
+		return res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/guilds/#guild_id/settings.ts b/src/api/routes/users/@me/guilds/#guild_id/settings.ts
index 7e9f2a08..ac6586ce 100644
--- a/src/api/routes/users/@me/guilds/#guild_id/settings.ts
+++ b/src/api/routes/users/@me/guilds/#guild_id/settings.ts
@@ -16,29 +16,49 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	Member,
 	OrmUtils,
 	UserGuildSettingsSchema,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
 // GET doesn't exist on discord.com
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const user = await Member.findOneOrFail({
-		where: { id: req.user_id, guild_id: req.params.guild_id },
-		select: ["settings"],
-	});
-	return res.json(user.settings);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await Member.findOneOrFail({
+			where: { id: req.user_id, guild_id: req.params.guild_id },
+			select: ["settings"],
+		});
+		return res.json(user.settings);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "UserGuildSettingsSchema" }),
+	route({
+		requestBody: "UserGuildSettingsSchema",
+		responses: {
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as UserGuildSettingsSchema;
 
diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts
index b3eeb964..8fe86265 100644
--- a/src/api/routes/users/@me/index.ts
+++ b/src/api/routes/users/@me/index.ts
@@ -16,36 +16,59 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
-	User,
-	PrivateUserProjection,
-	emitEvent,
-	UserUpdateEvent,
-	handleFile,
-	FieldErrors,
 	adjustEmail,
 	Config,
-	UserModifySchema,
+	emitEvent,
+	FieldErrors,
 	generateToken,
+	handleFile,
+	PrivateUserProjection,
+	User,
+	UserModifySchema,
+	UserUpdateEvent,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	res.json(
-		await User.findOne({
-			select: PrivateUserProjection,
-			where: { id: req.user_id },
-		}),
-	);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIPrivateUser",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		res.json(
+			await User.findOne({
+				select: PrivateUserProjection,
+				where: { id: req.user_id },
+			}),
+		);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "UserModifySchema" }),
+	route({
+		requestBody: "UserModifySchema",
+		responses: {
+			200: {
+				body: "UserUpdateResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as UserModifySchema;
 
diff --git a/src/api/routes/users/@me/mfa/codes-verification.ts b/src/api/routes/users/@me/mfa/codes-verification.ts
index 69d45e91..f71704a9 100644
--- a/src/api/routes/users/@me/mfa/codes-verification.ts
+++ b/src/api/routes/users/@me/mfa/codes-verification.ts
@@ -16,21 +16,34 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import {
 	BackupCode,
-	generateMfaBackupCodes,
-	User,
 	CodesVerificationSchema,
 	DiscordApiErrors,
+	User,
+	generateMfaBackupCodes,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "CodesVerificationSchema" }),
+	route({
+		requestBody: "CodesVerificationSchema",
+		responses: {
+			200: {
+				body: "APIBackupCodeArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		// const { key, nonce, regenerate } = req.body as CodesVerificationSchema;
 		const { regenerate } = req.body as CodesVerificationSchema;
diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts
index 4ddbf78e..f9cfc4c4 100644
--- a/src/api/routes/users/@me/mfa/codes.ts
+++ b/src/api/routes/users/@me/mfa/codes.ts
@@ -16,16 +16,16 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import {
 	BackupCode,
 	FieldErrors,
 	generateMfaBackupCodes,
-	User,
 	MfaCodesSchema,
+	User,
 } from "@spacebar/util";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
@@ -33,7 +33,23 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "MfaCodesSchema" }),
+	route({
+		requestBody: "MfaCodesSchema",
+		deprecated: true,
+		description:
+			"This route is replaced with users/@me/mfa/codes-verification in newer clients",
+		responses: {
+			200: {
+				body: "APIBackupCodeArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { password, regenerate } = req.body as MfaCodesSchema;
 
diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts
index 9f406423..362152d7 100644
--- a/src/api/routes/users/@me/mfa/totp/disable.ts
+++ b/src/api/routes/users/@me/mfa/totp/disable.ts
@@ -16,22 +16,32 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
-import { verifyToken } from "node-2fa";
-import { HTTPError } from "lambert-server";
 import {
-	User,
-	generateToken,
 	BackupCode,
 	TotpDisableSchema,
+	User,
+	generateToken,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+import { verifyToken } from "node-2fa";
 
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "TotpDisableSchema" }),
+	route({
+		requestBody: "TotpDisableSchema",
+		responses: {
+			200: {
+				body: "TokenOnlyResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as TotpDisableSchema;
 
diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts
index 4d6b2763..19836e4d 100644
--- a/src/api/routes/users/@me/mfa/totp/enable.ts
+++ b/src/api/routes/users/@me/mfa/totp/enable.ts
@@ -16,15 +16,15 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
+	TotpEnableSchema,
 	User,
-	generateToken,
 	generateMfaBackupCodes,
-	TotpEnableSchema,
+	generateToken,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { verifyToken } from "node-2fa";
 
@@ -32,7 +32,20 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "TotpEnableSchema" }),
+	route({
+		requestBody: "TotpEnableSchema",
+		responses: {
+			200: {
+				body: "TokenWithBackupCodesResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as TotpEnableSchema;
 
diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts
index 04aca7e4..9cf42def 100644
--- a/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts
+++ b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts
@@ -21,21 +21,31 @@ import { SecurityKey, User } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 const router = Router();
 
-router.delete("/", route({}), async (req: Request, res: Response) => {
-	const { key_id } = req.params;
+router.delete(
+	"/",
+	route({
+		responses: {
+			204: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { key_id } = req.params;
 
-	await SecurityKey.delete({
-		id: key_id,
-		user_id: req.user_id,
-	});
+		await SecurityKey.delete({
+			id: key_id,
+			user_id: req.user_id,
+		});
 
-	const keys = await SecurityKey.count({ where: { user_id: req.user_id } });
+		const keys = await SecurityKey.count({
+			where: { user_id: req.user_id },
+		});
 
-	// disable webauthn if there are no keys left
-	if (keys === 0)
-		await User.update({ id: req.user_id }, { webauthn_enabled: false });
+		// disable webauthn if there are no keys left
+		if (keys === 0)
+			await User.update({ id: req.user_id }, { webauthn_enabled: false });
 
-	res.sendStatus(204);
-});
+		res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts
index 29dbb7cf..f383ffb7 100644
--- a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts
+++ b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts
@@ -73,7 +73,17 @@ router.get("/", route({}), async (req: Request, res: Response) => {
 
 router.post(
 	"/",
-	route({ body: "WebAuthnPostSchema" }),
+	route({
+		requestBody: "WebAuthnPostSchema",
+		responses: {
+			200: {
+				body: "WebAuthnCreateResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		if (!WebAuthn.fido2) {
 			// TODO: I did this for typescript and I can't use !
diff --git a/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts
index d05c799c..248e61f9 100644
--- a/src/api/routes/users/@me/notes.ts
+++ b/src/api/routes/users/@me/notes.ts
@@ -16,71 +16,99 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
-import { User, Note, emitEvent, Snowflake } from "@spacebar/util";
+import { Note, Snowflake, User, emitEvent } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/:id", route({}), async (req: Request, res: Response) => {
-	const { id } = req.params;
-
-	const note = await Note.findOneOrFail({
-		where: {
-			owner: { id: req.user_id },
-			target: { id: id },
+router.get(
+	"/:id",
+	route({
+		responses: {
+			200: {
+				body: "UserNoteResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
 		},
-	});
+	}),
+	async (req: Request, res: Response) => {
+		const { id } = req.params;
+
+		const note = await Note.findOneOrFail({
+			where: {
+				owner: { id: req.user_id },
+				target: { id: id },
+			},
+		});
 
-	return res.json({
-		note: note?.content,
-		note_user_id: id,
-		user_id: req.user_id,
-	});
-});
+		return res.json({
+			note: note?.content,
+			note_user_id: id,
+			user_id: req.user_id,
+		});
+	},
+);
 
-router.put("/:id", route({}), async (req: Request, res: Response) => {
-	const { id } = req.params;
-	const owner = await User.findOneOrFail({ where: { id: req.user_id } });
-	const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw
-	const { note } = req.body;
+router.put(
+	"/:id",
+	route({
+		requestBody: "UserNoteUpdateSchema",
+		responses: {
+			204: {},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { id } = req.params;
+		const owner = await User.findOneOrFail({ where: { id: req.user_id } });
+		const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw
+		const { note } = req.body;
 
-	if (note && note.length) {
-		// upsert a note
-		if (
-			await Note.findOne({
-				where: { owner: { id: owner.id }, target: { id: target.id } },
-			})
-		) {
-			Note.update(
-				{ owner: { id: owner.id }, target: { id: target.id } },
-				{ owner, target, content: note },
-			);
+		if (note && note.length) {
+			// upsert a note
+			if (
+				await Note.findOne({
+					where: {
+						owner: { id: owner.id },
+						target: { id: target.id },
+					},
+				})
+			) {
+				Note.update(
+					{ owner: { id: owner.id }, target: { id: target.id } },
+					{ owner, target, content: note },
+				);
+			} else {
+				Note.insert({
+					id: Snowflake.generate(),
+					owner,
+					target,
+					content: note,
+				});
+			}
 		} else {
-			Note.insert({
-				id: Snowflake.generate(),
-				owner,
-				target,
-				content: note,
+			await Note.delete({
+				owner: { id: owner.id },
+				target: { id: target.id },
 			});
 		}
-	} else {
-		await Note.delete({
-			owner: { id: owner.id },
-			target: { id: target.id },
-		});
-	}
 
-	await emitEvent({
-		event: "USER_NOTE_UPDATE",
-		data: {
-			note: note,
-			id: target.id,
-		},
-		user_id: owner.id,
-	});
+		await emitEvent({
+			event: "USER_NOTE_UPDATE",
+			data: {
+				note: note,
+				id: target.id,
+			},
+			user_id: owner.id,
+		});
 
-	return res.status(204);
-});
+		return res.status(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts
index e9ea47e6..bce0a654 100644
--- a/src/api/routes/users/@me/relationships.ts
+++ b/src/api/routes/users/@me/relationships.ts
@@ -16,20 +16,20 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
-	RelationshipAddEvent,
-	User,
+	Config,
+	DiscordApiErrors,
 	PublicUserProjection,
-	RelationshipType,
+	Relationship,
+	RelationshipAddEvent,
 	RelationshipRemoveEvent,
+	RelationshipType,
+	User,
 	emitEvent,
-	Relationship,
-	Config,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { DiscordApiErrors } from "@spacebar/util";
-import { route } from "@spacebar/api";
 
 const router = Router();
 
@@ -38,29 +38,53 @@ const userProjection: (keyof User)[] = [
 	...PublicUserProjection,
 ];
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		relations: ["relationships", "relationships.to"],
-		select: ["id", "relationships"],
-	});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "UserRelationshipsResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			relations: ["relationships", "relationships.to"],
+			select: ["id", "relationships"],
+		});
 
-	//TODO DTO
-	const related_users = user.relationships.map((r) => {
-		return {
-			id: r.to.id,
-			type: r.type,
-			nickname: null,
-			user: r.to.toPublicUser(),
-		};
-	});
+		//TODO DTO
+		const related_users = user.relationships.map((r) => {
+			return {
+				id: r.to.id,
+				type: r.type,
+				nickname: null,
+				user: r.to.toPublicUser(),
+			};
+		});
 
-	return res.json(related_users);
-});
+		return res.json(related_users);
+	},
+);
 
 router.put(
 	"/:id",
-	route({ body: "RelationshipPutSchema" }),
+	route({
+		requestBody: "RelationshipPutSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		return await updateRelationship(
 			req,
@@ -77,7 +101,18 @@ router.put(
 
 router.post(
 	"/",
-	route({ body: "RelationshipPostSchema" }),
+	route({
+		requestBody: "RelationshipPostSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		return await updateRelationship(
 			req,
@@ -98,64 +133,78 @@ router.post(
 	},
 );
 
-router.delete("/:id", route({}), async (req: Request, res: Response) => {
-	const { id } = req.params;
-	if (id === req.user_id)
-		throw new HTTPError("You can't remove yourself as a friend");
+router.delete(
+	"/:id",
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { id } = req.params;
+		if (id === req.user_id)
+			throw new HTTPError("You can't remove yourself as a friend");
 
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		select: userProjection,
-		relations: ["relationships"],
-	});
-	const friend = await User.findOneOrFail({
-		where: { id: id },
-		select: userProjection,
-		relations: ["relationships"],
-	});
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			select: userProjection,
+			relations: ["relationships"],
+		});
+		const friend = await User.findOneOrFail({
+			where: { id: id },
+			select: userProjection,
+			relations: ["relationships"],
+		});
 
-	const relationship = user.relationships.find((x) => x.to_id === id);
-	const friendRequest = friend.relationships.find(
-		(x) => x.to_id === req.user_id,
-	);
+		const relationship = user.relationships.find((x) => x.to_id === id);
+		const friendRequest = friend.relationships.find(
+			(x) => x.to_id === req.user_id,
+		);
 
-	if (!relationship)
-		throw new HTTPError("You are not friends with the user", 404);
-	if (relationship?.type === RelationshipType.blocked) {
-		// unblock user
+		if (!relationship)
+			throw new HTTPError("You are not friends with the user", 404);
+		if (relationship?.type === RelationshipType.blocked) {
+			// unblock user
+
+			await Promise.all([
+				Relationship.delete({ id: relationship.id }),
+				emitEvent({
+					event: "RELATIONSHIP_REMOVE",
+					user_id: req.user_id,
+					data: relationship.toPublicRelationship(),
+				} as RelationshipRemoveEvent),
+			]);
+			return res.sendStatus(204);
+		}
+		if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
+			await Promise.all([
+				Relationship.delete({ id: friendRequest.id }),
+				await emitEvent({
+					event: "RELATIONSHIP_REMOVE",
+					data: friendRequest.toPublicRelationship(),
+					user_id: id,
+				} as RelationshipRemoveEvent),
+			]);
+		}
 
 		await Promise.all([
 			Relationship.delete({ id: relationship.id }),
 			emitEvent({
 				event: "RELATIONSHIP_REMOVE",
-				user_id: req.user_id,
 				data: relationship.toPublicRelationship(),
+				user_id: req.user_id,
 			} as RelationshipRemoveEvent),
 		]);
-		return res.sendStatus(204);
-	}
-	if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
-		await Promise.all([
-			Relationship.delete({ id: friendRequest.id }),
-			await emitEvent({
-				event: "RELATIONSHIP_REMOVE",
-				data: friendRequest.toPublicRelationship(),
-				user_id: id,
-			} as RelationshipRemoveEvent),
-		]);
-	}
 
-	await Promise.all([
-		Relationship.delete({ id: relationship.id }),
-		emitEvent({
-			event: "RELATIONSHIP_REMOVE",
-			data: relationship.toPublicRelationship(),
-			user_id: req.user_id,
-		} as RelationshipRemoveEvent),
-	]);
-
-	return res.sendStatus(204);
-});
+		return res.sendStatus(204);
+	},
+);
 
 export default router;
 
diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts
index 62cfe904..d22d6de1 100644
--- a/src/api/routes/users/@me/settings.ts
+++ b/src/api/routes/users/@me/settings.ts
@@ -16,23 +16,49 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
-import { User, UserSettingsSchema } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { User, UserSettingsSchema } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		relations: ["settings"],
-	});
-	return res.json(user.settings);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "UserSettings",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			relations: ["settings"],
+		});
+		return res.json(user.settings);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "UserSettingsSchema" }),
+	route({
+		requestBody: "UserSettingsSchema",
+		responses: {
+			200: {
+				body: "UserSettings",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as UserSettingsSchema;
 		if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale
diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts
index 59bac07f..10a8b21d 100644
--- a/src/api/routes/voice/regions.ts
+++ b/src/api/routes/voice/regions.ts
@@ -16,14 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { getIpAdress, route } from "@spacebar/api";
-import { getVoiceRegions } from "@spacebar/api";
+import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true?
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIGuildVoiceRegion",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true?
+	},
+);
 
 export default router;
diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts
index 604df4e9..5a0b48e6 100644
--- a/src/api/util/handlers/route.ts
+++ b/src/api/util/handlers/route.ts
@@ -17,21 +17,21 @@
 */
 
 import {
-	ajv,
 	DiscordApiErrors,
 	EVENT,
 	FieldErrors,
-	SpacebarApiErrors,
-	getPermission,
-	getRights,
-	normalizeBody,
 	PermissionResolvable,
 	Permissions,
 	RightResolvable,
 	Rights,
+	SpacebarApiErrors,
+	ajv,
+	getPermission,
+	getRights,
+	normalizeBody,
 } from "@spacebar/util";
-import { NextFunction, Request, Response } from "express";
 import { AnyValidateFunction } from "ajv/dist/core";
+import { NextFunction, Request, Response } from "express";
 
 declare global {
 	// TODO: fix this
@@ -52,21 +52,40 @@ export type RouteResponse = {
 export interface RouteOptions {
 	permission?: PermissionResolvable;
 	right?: RightResolvable;
-	body?: `${string}Schema`; // typescript interface name
-	test?: {
-		response?: RouteResponse;
-		body?: unknown;
-		path?: string;
-		event?: EVENT | EVENT[];
-		headers?: Record<string, string>;
+	requestBody?: `${string}Schema`; // typescript interface name
+	responses?: {
+		[status: number]: {
+			// body?: `${string}Response`;
+			body?: string;
+		};
+	};
+	event?: EVENT | EVENT[];
+	summary?: string;
+	description?: string;
+	query?: {
+		[key: string]: {
+			type: string;
+			required?: boolean;
+			description?: string;
+			values?: string[];
+		};
 	};
+	deprecated?: boolean;
+	// test?: {
+	// 	response?: RouteResponse;
+	// 	body?: unknown;
+	// 	path?: string;
+	// 	event?: EVENT | EVENT[];
+	// 	headers?: Record<string, string>;
+	// };
 }
 
 export function route(opts: RouteOptions) {
 	let validate: AnyValidateFunction | undefined;
-	if (opts.body) {
-		validate = ajv.getSchema(opts.body);
-		if (!validate) throw new Error(`Body schema ${opts.body} not found`);
+	if (opts.requestBody) {
+		validate = ajv.getSchema(opts.requestBody);
+		if (!validate)
+			throw new Error(`Body schema ${opts.requestBody} not found`);
 	}
 
 	return async (req: Request, res: Response, next: NextFunction) => {
diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts
index 172e9604..c51daf6c 100644
--- a/src/api/util/utility/ipAddress.ts
+++ b/src/api/util/utility/ipAddress.ts
@@ -102,7 +102,7 @@ export function getIpAdress(req: Request): string {
 	return (
 		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 		// @ts-ignore
-		req.headers[Config.get().security.forwadedFor] ||
+		req.headers[Config.get().security.forwardedFor] ||
 		req.socket.remoteAddress
 	);
 }