summary refs log tree commit diff
path: root/src/api/routes/guilds/#guild_id
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-07-28 08:24:15 +1000
committerMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-07-28 08:24:15 +1000
commit8a3989c29776ad7eba8077bf7cc9c56e28b9b8c3 (patch)
treee71d68052e6bf5ddfad64c643a8fb2d04c2e183c /src/api/routes/guilds/#guild_id
parentMerge branch 'master' into feat/refactorIdentify (diff)
parentMerge pull request #1075 from SpecificProtagonist/get_messages_around (diff)
downloadserver-8a3989c29776ad7eba8077bf7cc9c56e28b9b8c3.tar.xz
Merge branch 'master' into feat/refactorIdentify
Diffstat (limited to 'src/api/routes/guilds/#guild_id')
-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.ts85
-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.ts40
-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
26 files changed, 1414 insertions, 655 deletions
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..afe60614 100644
--- a/src/api/routes/guilds/#guild_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/index.ts
@@ -16,46 +16,79 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
 	DiscordApiErrors,
-	emitEvent,
-	getPermission,
-	getRights,
 	Guild,
 	GuildUpdateEvent,
-	handleFile,
-	Member,
 	GuildUpdateSchema,
+	Member,
 	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;
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..2a739683 100644
--- a/src/api/routes/guilds/#guild_id/welcome-screen.ts
+++ b/src/api/routes/guilds/#guild_id/welcome-screen.ts
@@ -16,27 +16,49 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
+import { Guild, GuildUpdateWelcomeScreenSchema, Member } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { Guild, Member, GuildUpdateWelcomeScreenSchema } from "@spacebar/util";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 
 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;
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;