Skip to content

Commit d857512

Browse files
committed
Add parseAndValidate helper and refactor all routes to use it
- Add parseAndValidate() helper to utils/validate.ts that combines JSON parsing + Zod validation with automatic HTTPException on errors - Refactor all route handlers to use parseAndValidate instead of manual c.req.json() + schema.parse() + try/catch ZodError pattern - Remove redundant ZodError catch blocks across all routes - Cleaner error handling with consistent 400 responses on validation errors Files affected: - utils/validate.ts: Added parseAndValidate helper - All route files: Replaced manual validation pattern with parseAndValidate Bundle size: 584.9kb (slightly reduced from 585.9kb)
1 parent 69f3915 commit d857512

12 files changed

Lines changed: 83 additions & 135 deletions

File tree

src/routes/api-keys.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Hono } from 'hono';
2-
import { z, ZodError } from 'zod';
2+
import { z } from 'zod';
33
import { createApiKey, listApiKeys, deleteApiKey } from '../services/db/api-keys';
4+
import { parseAndValidate } from '../utils/validate';
45

56
const apiKeys = new Hono();
67

@@ -14,8 +15,7 @@ const CreateApiKeySchema = z.object({
1415
apiKeys.post('/', async (c) => {
1516
try {
1617
const userId = c.get('userId');
17-
const body = await c.req.json();
18-
const input = CreateApiKeySchema.parse(body);
18+
const input = await parseAndValidate(c, CreateApiKeySchema);
1919

2020
const expiresAt = input.expiresAt ? new Date(input.expiresAt) : undefined;
2121
const { apiKey, token } = await createApiKey(userId, input.name, expiresAt);
@@ -33,10 +33,6 @@ apiKeys.post('/', async (c) => {
3333
201
3434
);
3535
} catch (error) {
36-
if (error instanceof ZodError) {
37-
return c.json({ error: 'Invalid input', details: error.issues }, 400);
38-
}
39-
4036
console.error('Failed to create API key:', error);
4137
return c.json({ error: 'Failed to create API key' }, 500);
4238
}

src/routes/auth.ts

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Hono } from 'hono';
22
import { setCookie } from 'hono/cookie';
33
import { verify } from 'hono/jwt';
4-
import { ZodError } from 'zod';
54

65
import { authService } from '../services/auth';
76
import { github as githubConfig, jwt as jwtConfig } from '../config';
@@ -14,7 +13,7 @@ import {
1413
ResetPasswordInputSchema,
1514
ResendSetPasswordInputSchema
1615
} from '../types';
17-
import { jsonResponse } from '../utils/validate';
16+
import { jsonResponse, parseAndValidate } from '../utils/validate';
1817

1918
const auth = new Hono();
2019

@@ -134,8 +133,7 @@ auth.get('/callback/github', async (c) => {
134133
// Step 1: Register with email only (sends set-password link)
135134
auth.post('/register', async (c) => {
136135
try {
137-
const body = await c.req.json();
138-
const input = RegisterInputSchema.parse(body);
136+
const input = await parseAndValidate(c, RegisterInputSchema);
139137

140138
await authService.registerWithEmail(input.email);
141139

@@ -146,10 +144,6 @@ auth.post('/register', async (c) => {
146144
201
147145
);
148146
} catch (error) {
149-
if (error instanceof ZodError) {
150-
return c.json({ error: 'Invalid email format' }, 400);
151-
}
152-
153147
if (error instanceof Error && error.message === 'Email already registered') {
154148
return c.json({ error: 'Email already registered' }, 409);
155149
}
@@ -162,8 +156,7 @@ auth.post('/register', async (c) => {
162156
// Step 2: Set password using token from email
163157
auth.post('/set-password', async (c) => {
164158
try {
165-
const body = await c.req.json();
166-
const input = SetPasswordInputSchema.parse(body);
159+
const input = await parseAndValidate(c, SetPasswordInputSchema);
167160

168161
const user = await authService.setPassword(input.token, input.password);
169162
const tokens = await authService.createTokens(user);
@@ -176,10 +169,6 @@ auth.post('/set-password', async (c) => {
176169

177170
return jsonResponse(c, LoginResponseSchema, tokens);
178171
} catch (error) {
179-
if (error instanceof ZodError) {
180-
return c.json({ error: 'Invalid token or password format' }, 400);
181-
}
182-
183172
if (error instanceof Error && error.message === 'Invalid or expired token') {
184173
return c.json({ error: 'Invalid or expired link' }, 400);
185174
}
@@ -192,8 +181,7 @@ auth.post('/set-password', async (c) => {
192181
// Login with email and password
193182
auth.post('/login', async (c) => {
194183
try {
195-
const body = await c.req.json();
196-
const input = LoginInputSchema.parse(body);
184+
const input = await parseAndValidate(c, LoginInputSchema);
197185

198186
const user = await authService.loginWithPassword(input.email, input.password);
199187
const tokens = await authService.createTokens(user);
@@ -206,10 +194,6 @@ auth.post('/login', async (c) => {
206194

207195
return jsonResponse(c, LoginResponseSchema, tokens);
208196
} catch (error) {
209-
if (error instanceof ZodError) {
210-
return c.json({ error: 'Invalid email or password format' }, 400);
211-
}
212-
213197
if (error instanceof Error && error.message === 'Invalid credentials') {
214198
return c.json({ error: 'Invalid email or password' }, 401);
215199
}
@@ -222,8 +206,7 @@ auth.post('/login', async (c) => {
222206
// Request password reset
223207
auth.post('/forgot-password', async (c) => {
224208
try {
225-
const body = await c.req.json();
226-
const input = ForgotPasswordInputSchema.parse(body);
209+
const input = await parseAndValidate(c, ForgotPasswordInputSchema);
227210

228211
await authService.requestPasswordReset(input.email);
229212

@@ -232,10 +215,6 @@ auth.post('/forgot-password', async (c) => {
232215
message: 'If an account exists with this email, you will receive a password reset link.'
233216
});
234217
} catch (error) {
235-
if (error instanceof ZodError) {
236-
return c.json({ error: 'Invalid email format' }, 400);
237-
}
238-
239218
console.error('Password reset request error:', error);
240219
return c.json({
241220
message: 'If an account exists with this email, you will receive a password reset link.'
@@ -246,8 +225,7 @@ auth.post('/forgot-password', async (c) => {
246225
// Reset password with token
247226
auth.post('/reset-password', async (c) => {
248227
try {
249-
const body = await c.req.json();
250-
const input = ResetPasswordInputSchema.parse(body);
228+
const input = await parseAndValidate(c, ResetPasswordInputSchema);
251229

252230
const user = await authService.resetPassword(input.token, input.password);
253231
const tokens = await authService.createTokens(user);
@@ -260,10 +238,6 @@ auth.post('/reset-password', async (c) => {
260238

261239
return jsonResponse(c, LoginResponseSchema, tokens);
262240
} catch (error) {
263-
if (error instanceof ZodError) {
264-
return c.json({ error: 'Invalid token or password format' }, 400);
265-
}
266-
267241
if (error instanceof Error && error.message === 'Invalid or expired token') {
268242
return c.json({ error: 'Invalid or expired reset link' }, 400);
269243
}
@@ -276,19 +250,14 @@ auth.post('/reset-password', async (c) => {
276250
// Resend set-password email
277251
auth.post('/resend-set-password', async (c) => {
278252
try {
279-
const body = await c.req.json();
280-
const input = ResendSetPasswordInputSchema.parse(body);
253+
const input = await parseAndValidate(c, ResendSetPasswordInputSchema);
281254

282255
await authService.resendSetPasswordEmail(input.email);
283256

284257
return c.json({
285258
message: 'If a pending account exists with this email, a new link has been sent.'
286259
});
287260
} catch (error) {
288-
if (error instanceof ZodError) {
289-
return c.json({ error: 'Invalid email format' }, 400);
290-
}
291-
292261
console.error('Resend set-password error:', error);
293262
return c.json({
294263
message: 'If a pending account exists with this email, a new link has been sent.'

src/routes/crons.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@ import { Hono } from 'hono';
22
import { cronsService } from '../services/crons';
33
import { workersService } from '../services/workers';
44
import { CronCreateInputSchema, CronUpdateInputSchema, CronSchema, WorkerSchema } from '../types';
5-
import { jsonResponse } from '../utils/validate';
5+
import { jsonResponse, parseAndValidate } from '../utils/validate';
66

77
const crons = new Hono();
88

99
// PATCH /crons/:id - Update cron
1010
crons.patch('/:id', async (c) => {
1111
const userId = c.get('userId');
1212
const id = c.req.param('id');
13-
const body = await c.req.json();
13+
const payload = await parseAndValidate(c, CronUpdateInputSchema);
1414

1515
try {
16-
const payload = CronUpdateInputSchema.parse(body);
1716
const cron = await cronsService.update(userId, id, payload);
1817

1918
// Return updated worker
@@ -62,10 +61,9 @@ crons.delete('/:id', async (c) => {
6261
// POST /crons - Create cron
6362
crons.post('/', async (c) => {
6463
const userId = c.get('userId');
65-
const body = await c.req.json();
64+
const payload = await parseAndValidate(c, CronCreateInputSchema);
6665

6766
try {
68-
const payload = CronCreateInputSchema.parse(body);
6967
const cron = await cronsService.create(userId, payload);
7068
return jsonResponse(c, CronSchema, cron, 201);
7169
} catch (error) {

src/routes/database-tokens.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Hono } from 'hono';
2-
import { z, ZodError } from 'zod';
2+
import { z } from 'zod';
33
import { createDatabaseToken, listDatabaseTokens, deleteDatabaseToken } from '../services/db/database-tokens';
44
import { findDatabaseById, findDatabaseByName } from '../services/db/databases';
55
import { DatabaseOperations } from '../types/schemas/database-token.schema';
6+
import { parseAndValidate } from '../utils/validate';
67

78
const databaseTokens = new Hono();
89

@@ -58,12 +59,11 @@ databaseTokens.get('/:id/tokens', async (c) => {
5859

5960
// POST /databases/:id/tokens - Create a new token
6061
databaseTokens.post('/:id/tokens', async (c) => {
61-
try {
62-
const userId = c.get('userId');
63-
const idOrName = c.req.param('id');
64-
const body = await c.req.json();
65-
const input = CreateTokenSchema.parse(body);
62+
const userId = c.get('userId');
63+
const idOrName = c.req.param('id');
64+
const input = await parseAndValidate(c, CreateTokenSchema);
6665

66+
try {
6767
// Verify database ownership
6868
const database = await findDatabase(userId, idOrName);
6969

@@ -87,10 +87,6 @@ databaseTokens.post('/:id/tokens', async (c) => {
8787
201
8888
);
8989
} catch (error) {
90-
if (error instanceof ZodError) {
91-
return c.json({ error: 'Invalid input', details: error.issues }, 400);
92-
}
93-
9490
// Handle unique constraint violation (duplicate name)
9591
if (error instanceof Error && error.message.includes('unique constraint')) {
9692
return c.json({ error: 'A token with this name already exists for this database' }, 409);

src/routes/databases.ts

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Hono } from 'hono';
2-
import { z, ZodError } from 'zod';
2+
import { z } from 'zod';
33
import { databasesService } from '../services/databases';
44
import { getSystemToken } from '../services/db/database-tokens';
55
import { PostgateClient } from '../services/postgate';
66
import { postgate as postgateConfig } from '../config';
77
import { DatabaseSchema, DatabaseCreateInputSchema } from '../types';
8-
import { jsonResponse, jsonArrayResponse } from '../utils/validate';
8+
import { jsonResponse, jsonArrayResponse, parseAndValidate } from '../utils/validate';
99
import tables from './tables';
1010

1111
const databases = new Hono();
@@ -48,23 +48,12 @@ databases.get('/:id', async (c) => {
4848
// POST /databases - Create new database
4949
databases.post('/', async (c) => {
5050
const userId = c.get('userId');
51-
const body = await c.req.json();
51+
const payload = await parseAndValidate(c, DatabaseCreateInputSchema);
5252

5353
try {
54-
const payload = DatabaseCreateInputSchema.parse(body);
5554
const db = await databasesService.create(userId, payload);
5655
return jsonResponse(c, DatabaseSchema, db, 201);
5756
} catch (error) {
58-
if (error instanceof z.ZodError) {
59-
return c.json(
60-
{
61-
error: 'Validation error',
62-
details: error.issues
63-
},
64-
400
65-
);
66-
}
67-
6857
console.error('Failed to create database:', error);
6958
return c.json(
7059
{
@@ -80,7 +69,6 @@ databases.post('/', async (c) => {
8069
databases.patch('/:id', async (c) => {
8170
const userId = c.get('userId');
8271
const idOrName = c.req.param('id');
83-
const body = await c.req.json();
8472

8573
const UpdateSchema = z.object({
8674
name: z.string().min(1).max(100).trim().optional(),
@@ -89,14 +77,15 @@ databases.patch('/:id', async (c) => {
8977
timeoutSeconds: z.number().int().positive().max(300).optional()
9078
});
9179

80+
const payload = await parseAndValidate(c, UpdateSchema);
81+
9282
try {
9383
const existing = await databasesService.findByIdOrName(userId, idOrName);
9484

9585
if (!existing) {
9686
return c.json({ error: 'Database not found' }, 404);
9787
}
9888

99-
const payload = UpdateSchema.parse(body);
10089
const db = await databasesService.update(userId, existing.id, payload);
10190

10291
if (!db) {
@@ -105,16 +94,6 @@ databases.patch('/:id', async (c) => {
10594

10695
return jsonResponse(c, DatabaseSchema, db);
10796
} catch (error) {
108-
if (error instanceof z.ZodError) {
109-
return c.json(
110-
{
111-
error: 'Validation error',
112-
details: error.issues
113-
},
114-
400
115-
);
116-
}
117-
11897
console.error('Failed to update database:', error);
11998
return c.json({ error: 'Failed to update database' }, 500);
12099
}
@@ -153,21 +132,18 @@ const ExecSchema = z.object({
153132

154133
// POST /databases/:id/exec - Execute SQL on a database
155134
databases.post('/:id/exec', async (c) => {
156-
try {
157-
const userId = c.get('userId');
158-
const idOrName = c.req.param('id');
135+
const userId = c.get('userId');
136+
const idOrName = c.req.param('id');
137+
const { sql, params } = await parseAndValidate(c, ExecSchema);
159138

139+
try {
160140
// Verify database ownership
161141
const database = await databasesService.findByIdOrName(userId, idOrName);
162142

163143
if (!database) {
164144
return c.json({ error: 'Database not found' }, 404);
165145
}
166146

167-
// Parse request body
168-
const body = await c.req.json();
169-
const { sql, params } = ExecSchema.parse(body);
170-
171147
// Get or create system token for this database
172148
const systemToken = await getSystemToken(database.id);
173149

@@ -180,10 +156,6 @@ databases.post('/:id/exec', async (c) => {
180156
rowCount: result.row_count
181157
});
182158
} catch (error) {
183-
if (error instanceof ZodError) {
184-
return c.json({ error: 'Invalid input', details: error.issues }, 400);
185-
}
186-
187159
console.error('SQL execution failed:', error);
188160
const message = error instanceof Error ? error.message : 'Unknown error';
189161
return c.json({ error: message }, 500);

src/routes/domains.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Hono } from 'hono';
22
import { domainsService } from '../services/domains';
33
import { DomainSchema, DomainCreateInputSchema } from '../types';
4-
import { jsonResponse, jsonArrayResponse } from '../utils/validate';
4+
import { jsonResponse, jsonArrayResponse, parseAndValidate } from '../utils/validate';
55

66
const domains = new Hono();
77

@@ -20,10 +20,9 @@ domains.get('/', async (c) => {
2020
// POST /domains - Create domain
2121
domains.post('/', async (c) => {
2222
const userId = c.get('userId');
23-
const body = await c.req.json();
23+
const payload = await parseAndValidate(c, DomainCreateInputSchema);
2424

2525
try {
26-
const payload = DomainCreateInputSchema.parse(body);
2726
const domain = await domainsService.create(userId, payload);
2827
return jsonResponse(c, DomainSchema, domain, 201);
2928
} catch (error) {

0 commit comments

Comments
 (0)