api: apply a better structure to handle zod errors
Parents:
7e3a6c413 file(s) changed
- api/nodemon.json +1 -2
- api/src/controllers/auth.ts +5 -12
- api/src/controllers/careActions.ts +3 -2
- api/src/controllers/insights.ts +2 -4
- api/src/controllers/medicineRegimens.ts +4 -3
- api/src/controllers/moods.ts +2 -3
- api/src/controllers/sleepRecords.ts +3 -6
- api/src/controllers/triggerMoodLinks.ts +2 -1
- api/src/controllers/triggers.ts +4 -7
- api/src/controllers/users.ts +4 -7
- api/src/lib/errors/validationError.ts +16 -0
- api/tests/moods/create.test.ts +2 -1
- api/tests/users/create.test.ts +4 -3
api/nodemon.json
1 1
{
2 2
"watch": ["src"],
3 3
"ext": "ts",
4 -
"exec": "node -r ts-node/register -r tsconfig-paths/register ./src/index.ts"
4 +
"exec": "tsx ./src/index.ts"
5 5
}
6 -
api/src/controllers/auth.ts
1 1
import { Request, Response, NextFunction } from "express";
2 2
import { AuthenticatedRequest } from "@app/middleware/auth";
3 3
4 4
import {
5 5
login,
▸ 16 unchanged lines
22 22
LoginSchema,
23 23
PasswordResetSchema,
24 24
PasswordResetRequestSchema,
25 25
ActivateUserSchema,
26 26
} from "@app/schemas";
27 +
import { formatValidationError } from "@app/lib/errors/validationError";
27 28
28 29
import { addMinutes } from "date-fns";
29 30
import { getQueue, MailJobName } from "@app/lib/queue";
30 31
import crypto from "crypto";
31 32
▸ 1 unchanged lines
33 34
login: async (req: Request, res: Response, next: NextFunction) => {
34 35
try {
35 36
const parsed = LoginSchema.safeParse(req.body);
36 37
37 38
if (!parsed.success) {
38 -
return res.status(400).json({
39 -
errors: parsed.error!.issues,
40 -
});
39 +
return res.status(400).json(formatValidationError(parsed.error!));
41 40
}
42 41
43 42
const { user, token } = await login(parsed.data);
44 43
45 44
if (
▸ 27 unchanged lines
73 72
forgotPassword: async (req: Request, res: Response, next: NextFunction) => {
74 73
try {
75 74
const parsed = PasswordResetRequestSchema.safeParse(req.body);
76 75
77 76
if (!parsed.success) {
78 -
return res.status(400).json({
79 -
errors: parsed.error!.issues,
80 -
});
77 +
return res.status(400).json(formatValidationError(parsed.error!));
81 78
}
82 79
83 80
const result = await requestPasswordReset(parsed.data);
84 81
return res.status(200).json(result);
85 82
} catch (err) {
▸ 4 unchanged lines
90 87
resetPassword: async (req: Request, res: Response, next: NextFunction) => {
91 88
try {
92 89
const parsed = PasswordResetSchema.safeParse(req.body);
93 90
94 91
if (!parsed.success) {
95 -
return res.status(400).json({
96 -
errors: parsed.error!.issues,
97 -
});
92 +
return res.status(400).json(formatValidationError(parsed.error!));
98 93
}
99 94
100 95
const result = await resetPassword(parsed.data);
101 96
return res.status(200).json(result);
102 97
} catch (err: any) {
▸ 54 unchanged lines
157 152
activate: async (req: Request, res: Response, next: NextFunction) => {
158 153
try {
159 154
const parsed = ActivateUserSchema.safeParse(req.body);
160 155
161 156
if (!parsed.success) {
162 -
return res.status(400).json({
163 -
errors: parsed.error!.issues,
164 -
});
157 +
return res.status(400).json(formatValidationError(parsed.error!));
165 158
}
166 159
167 160
const user = await findUserByActivationCode(String(parsed.data.code));
168 161
169 162
if (!user) {
▸ 43 unchanged lines
213 206
} catch (err: any) {
214 207
next(err);
215 208
}
216 209
},
217 210
};
api/src/controllers/careActions.ts
1 1
import { Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import { CareActionType } from '@prisma/client';
4 4
import { CreateCareActionSchema, PatchCareActionSchema } from '@app/schemas';
5 +
import { formatValidationError } from '@app/lib/errors/validationError';
5 6
import {
6 7
getCareActionsByUserId,
7 8
getCareActionById,
8 9
createCareAction,
9 10
destroyCareActionById,
▸ 40 unchanged lines
50 51
51 52
req.log.info("Params: %s", req.body.careAction)
52 53
req.log.info("Care action: %s", parsed.error)
53 54
54 55
if (!parsed.success) {
55 -
return res.status(400).json({ errors: parsed.error!.issues });
56 +
return res.status(400).json(formatValidationError(parsed.error!));
56 57
}
57 58
58 59
const careAction = await createCareAction(req.userId!, parsed.data);
59 60
60 61
return res.status(201).json({ careAction });
▸ 15 unchanged lines
76 77
patch: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
77 78
try {
78 79
const parsed = PatchCareActionSchema.safeParse(req.body.careAction);
79 80
80 81
if (!parsed.success) {
81 -
return res.status(400).json({ errors: parsed.error.issues });
82 +
return res.status(400).json(formatValidationError(parsed.error!));
82 83
}
83 84
84 85
const careAction = await patchCareActionById(
85 86
req.userId!,
86 87
Number(req.params.id),
▸ 8 unchanged lines
95 96
} catch (err) {
96 97
next(err);
97 98
}
98 99
},
99 100
};
api/src/controllers/insights.ts
1 1
import { Response, NextFunction } from "express";
2 2
import { AuthenticatedRequest } from "@app/middleware/auth";
3 3
import { getInsightsByUserId } from "@app/services/insight.service";
4 4
import { InsightsQuerySchema } from "@app/schemas";
5 +
import { formatValidationError } from "@app/lib/errors/validationError";
5 6
6 7
export const InsightsController = {
7 8
index: async (
8 9
req: AuthenticatedRequest,
9 10
res: Response,
▸ 1 unchanged lines
11 12
) => {
12 13
try {
13 14
const parsedQuery = InsightsQuerySchema.safeParse(req.query);
14 15
15 16
if (!parsedQuery.success) {
16 -
return res.status(400).json({
17 -
message: "Invalid query parameters",
18 -
errors: parsedQuery.error.flatten(),
19 -
});
17 +
return res.status(400).json(formatValidationError(parsedQuery.error));
20 18
}
21 19
22 20
const insights = await getInsightsByUserId(req.userId!, {
23 21
type: parsedQuery.data.type,
24 22
period: parsedQuery.data.period,
▸ 4 unchanged lines
29 27
} catch (err) {
30 28
next(err);
31 29
}
32 30
},
33 31
};
api/src/controllers/medicineRegimens.ts
1 1
import { Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import { CreateMedicineRegimenSchema, UpdateMedicineRegimenSchema } from '@app/schemas';
4 4
import { ToggleMedicineRegimenSchema } from '@app/schemas/medicineRegimen.schema';
5 +
import { formatValidationError } from '@app/lib/errors/validationError';
5 6
import {
6 7
getMedicineRegimensByUserId,
7 8
getMedicineRegimenById,
8 9
createMedicineRegimen,
9 10
updateMedicineRegimen,
▸ 38 unchanged lines
48 49
create: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
49 50
try {
50 51
const parsed = CreateMedicineRegimenSchema.safeParse(req.body.regimen);
51 52
52 53
if (!parsed.success) {
53 -
return res.status(400).json({ errors: parsed.error!.issues });
54 +
return res.status(400).json(formatValidationError(parsed.error!));
54 55
}
55 56
56 57
const regimen = await createMedicineRegimen(req.userId!, parsed.data);
57 58
58 59
return res.status(201).json({ regimen });
▸ 5 unchanged lines
64 65
update: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
65 66
try {
66 67
const parsed = UpdateMedicineRegimenSchema.safeParse(req.body.regimen);
67 68
68 69
if (!parsed.success) {
69 -
return res.status(400).json({ errors: parsed.error!.issues });
70 +
return res.status(400).json(formatValidationError(parsed.error!));
70 71
}
71 72
72 73
const regimen = await updateMedicineRegimen(
73 74
req.userId!,
74 75
Number(req.params.id),
▸ 9 unchanged lines
84 85
toggle: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
85 86
try {
86 87
const parsed = ToggleMedicineRegimenSchema.safeParse(req.body);
87 88
88 89
if (!parsed.success) {
89 -
return res.status(400).json({ errors: parsed.error!.issues });
90 +
return res.status(400).json(formatValidationError(parsed.error!));
90 91
}
91 92
92 93
const regimen = await toggleMedicineRegimen(
93 94
req.userId!,
94 95
Number(req.params.id),
▸ 14 unchanged lines
109 110
} catch (err) {
110 111
next(err);
111 112
}
112 113
},
113 114
};
api/src/controllers/moods.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import { User } from '@prisma/client';
4 4
import {
5 5
createMood,
▸ 6 unchanged lines
12 12
import { sendPushNotification } from '@app/lib/sendPushNotification';
13 13
14 14
import {
15 15
CreateMoodSchema
16 16
} from '@app/schemas'
17 +
import { formatValidationError } from '@app/lib/errors/validationError'
17 18
18 19
export const MoodsController = {
19 20
create: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
20 21
try {
21 22
const parsed = CreateMoodSchema.safeParse(req.body.mood);
22 23
23 24
if (!parsed.success) {
24 -
return res.status(400).json({
25 -
errors: parsed.error!.issues
26 -
});
25 +
return res.status(400).json(formatValidationError(parsed.error!));
27 26
}
28 27
29 28
const mood = await createMood(req.userId!, parsed.data);
30 29
31 30
return res.status(201).json({ mood });
▸ 68 unchanged lines
100 99
} catch (err) {
101 100
next(err);
102 101
}
103 102
}
104 103
};
api/src/controllers/sleepRecords.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import {
4 4
createSleepRecord,
5 5
getSleepRecordsByUserId,
▸ 4 unchanged lines
10 10
11 11
import {
12 12
CreateSleepRecordSchema,
13 13
UpdateSleepRecordSchema
14 14
} from '@app/schemas'
15 +
import { formatValidationError } from '@app/lib/errors/validationError'
15 16
16 17
export const SleepRecordsController = {
17 18
create: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
18 19
try {
19 20
const parsed = CreateSleepRecordSchema.safeParse(req.body.sleepRecord);
20 21
21 22
if (!parsed.success) {
22 -
return res.status(400).json({
23 -
errors: parsed.error!.issues
24 -
});
23 +
return res.status(400).json(formatValidationError(parsed.error!));
25 24
}
26 25
27 26
const sleepRecord = await createSleepRecord(req.userId!, parsed.data);
28 27
29 28
return res.status(201).json({ sleepRecord });
▸ 42 unchanged lines
72 71
update: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
73 72
try {
74 73
const parsed = UpdateSleepRecordSchema.safeParse(req.body.sleepRecord);
75 74
76 75
if (!parsed.success) {
77 -
return res.status(400).json({
78 -
errors: parsed.error!.issues
79 -
});
76 +
return res.status(400).json(formatValidationError(parsed.error!));
80 77
}
81 78
82 79
const sleepRecord = await updateSleepRecordById(req.userId!, Number(req.params.id!), parsed.data);
83 80
84 81
return res.status(200).json({ sleepRecord });
85 82
} catch (err) {
86 83
next(err);
87 84
}
88 85
}
89 86
};
api/src/controllers/triggerMoodLinks.ts
1 1
import { Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import { LinkMoodSchema } from '@app/schemas';
4 +
import { formatValidationError } from '@app/lib/errors/validationError';
4 5
import {
5 6
linkTriggerToMood,
6 7
unlinkTriggerFromMood,
7 8
} from '@app/services/triggerMoodLink.service';
8 9
▸ 1 unchanged lines
10 11
linkMood: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
11 12
try {
12 13
const parsed = LinkMoodSchema.safeParse(req.body);
13 14
14 15
if (!parsed.success) {
15 -
return res.status(400).json({ errors: parsed.error!.issues });
16 +
return res.status(400).json(formatValidationError(parsed.error!));
16 17
}
17 18
18 19
const triggerId = Number(req.params.triggerId);
19 20
const { moodId, perceivedImpact } = parsed.data;
20 21
▸ 16 unchanged lines
37 38
} catch (err) {
38 39
next(err);
39 40
}
40 41
},
41 42
};
api/src/controllers/triggers.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import {
4 4
createTrigger,
5 5
getTriggersByUserId,
▸ 9 unchanged lines
15 15
import {
16 16
CreateTriggerSchema,
17 17
UpdateTriggerSchema,
18 18
LinkMoodSchema,
19 19
} from '@app/schemas'
20 +
import { formatValidationError } from '@app/lib/errors/validationError'
20 21
21 22
export const TriggersController = {
22 23
create: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
23 24
try {
24 25
const parsed = CreateTriggerSchema.safeParse(req.body.trigger);
25 26
26 27
if (!parsed.success) {
27 -
return res.status(400).json({
28 -
errors: parsed.error!.issues
29 -
});
28 +
return res.status(400).json(formatValidationError(parsed.error!));
30 29
}
31 30
32 31
const trigger = await createTrigger(req.userId!, parsed.data);
33 32
34 33
return res.status(201).json({ trigger });
▸ 42 unchanged lines
77 76
update: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
78 77
try {
79 78
const parsed = UpdateTriggerSchema.safeParse(req.body.trigger);
80 79
81 80
if (!parsed.success) {
82 -
return res.status(400).json({
83 -
errors: parsed.error!.issues
84 -
});
81 +
return res.status(400).json(formatValidationError(parsed.error!));
85 82
}
86 83
87 84
const trigger = await updateTriggerById(req.userId!, Number(req.params.id!), parsed.data);
88 85
89 86
return res.status(200).json({ trigger });
▸ 5 unchanged lines
95 92
linkMood: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
96 93
try {
97 94
const parsed = LinkMoodSchema.safeParse(req.body);
98 95
99 96
if (!parsed.success) {
100 -
return res.status(400).json({ errors: parsed.error!.issues });
97 +
return res.status(400).json(formatValidationError(parsed.error!));
101 98
}
102 99
103 100
const triggerId = Number(req.params.triggerId);
104 101
const { moodId, perceivedImpact } = parsed.data;
105 102
▸ 16 unchanged lines
122 119
} catch (err) {
123 120
next(err);
124 121
}
125 122
},
126 123
};
api/src/controllers/users.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import {
3 3
generateToken
4 4
} from '@app/services/auth.service';
5 5
▸ 2 unchanged lines
8 8
import {
9 9
CreateUserSchema,
10 10
UpdateUserSchema,
11 11
UpdateUserPreferencesSchema,
12 12
} from '@app/schemas';
13 +
import { formatValidationError } from '@app/lib/errors/validationError';
13 14
import { syncDailyReminderJob } from '@app/services/dailyReminder.sync';
14 15
15 16
import {
16 17
createUser,
17 18
findUserById,
▸ 24 unchanged lines
42 43
create: async (req: Request, res: Response, next: NextFunction) => {
43 44
try {
44 45
const parsed = CreateUserSchema.safeParse(req.body.user);
45 46
46 47
if (!parsed.success) {
47 -
return res.status(422).json({
48 -
errors: parsed.error!.issues
49 -
})
48 +
return res.status(400).json(formatValidationError(parsed.error!));
50 49
}
51 50
52 51
const user = await createUser(parsed.data);
53 52
const jwtToken = generateToken(user.id, user.email);
54 53
▸ 21 unchanged lines
76 75
try {
77 76
const { id } = req.params;
78 77
79 78
const parsed = UpdateUserSchema.safeParse(req.body.user);
80 79
if (!parsed.success) {
81 -
return res.status(400).json({
82 -
errors: parsed.error!.issues
83 -
});
80 +
return res.status(400).json(formatValidationError(parsed.error!));
84 81
}
85 82
86 83
const user = await findUserById(Number(id));
87 84
88 85
if (!user) {
▸ 52 unchanged lines
141 138
142 139
updatePreferences: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
143 140
try {
144 141
const parsed = UpdateUserPreferencesSchema.safeParse(req.body.user);
145 142
if (!parsed.success) {
146 -
return res.status(400).json({ errors: parsed.error!.issues });
143 +
return res.status(400).json(formatValidationError(parsed.error!));
147 144
}
148 145
149 146
const user = await prisma.user.update({
150 147
where: { id: req.userId },
151 148
data: parsed.data,
▸ 59 unchanged lines
211 208
return res.json({
212 209
user
213 210
})
214 211
}
215 212
}
api/src/lib/errors/validationError.ts
@@ -0,0 +1,16 @@
1 + import { ZodError } from 'zod';
2 + import { fromZodError } from 'zod-validation-error';
3 +
4 + export function formatValidationError(err: ZodError) {
5 + const fields: Record<string, string> = {};
6 +
7 + for (const issue of err.issues) {
8 + const key = issue.path.join('.') || '_form';
9 + if (!fields[key]) fields[key] = issue.message;
10 + }
11 +
12 + return {
13 + error: fromZodError(err).message,
14 + fields,
15 + };
16 + }
api/tests/moods/create.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 2
import request from 'supertest'
3 3
import { createApp } from '@app/createApp'
4 4
import { buildPrisma, cleanupTestDb, createTestUser, generateTestToken } from '../test-helper'
5 5
▸ 39 unchanged lines
45 45
.post('/moods')
46 46
.set('Authorization', `Bearer ${token}`)
47 47
.send({ mood: { selectedMood: 'GOOD' } })
48 48
49 49
expect(res.status).toBe(400)
50 -
expect(res.body.errors).toBeTruthy()
50 +
expect(res.body.error).toBeTruthy()
51 +
expect(res.body.fields).toBeTruthy()
51 52
})
52 53
53 54
it('returns 401 without token', async () => {
54 55
const res = await request(app).post('/moods').send({ mood: validMood })
55 56
expect(res.status).toBe(401)
56 57
})
57 58
})
api/tests/users/create.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 2
import request from 'supertest'
3 3
import { createApp } from '@app/createApp'
4 4
import { buildPrisma, cleanupTestDb } from '../test-helper'
5 5
▸ 27 unchanged lines
33 33
.send({ user: { email: 'dup@example.com', firstName: 'B', password: 'senha123' } })
34 34
35 35
expect(res.status).toBe(409)
36 36
})
37 37
38 -
it('returns 422 for missing required fields', async () => {
38 +
it('returns 400 for missing required fields', async () => {
39 39
const res = await request(app)
40 40
.post('/users')
41 41
.send({ user: { email: 'x@example.com' } })
42 42
43 -
expect(res.status).toBe(422)
44 -
expect(res.body.errors).toBeTruthy()
43 +
expect(res.status).toBe(400)
44 +
expect(res.body.error).toBeTruthy()
45 +
expect(res.body.fields).toBeTruthy()
45 46
})
46 47
})