This is a simple implementation of Django's sample project "Poll" using and express.
The controller code:
export default class AuthController {
private db: AuthDb;
constructor(db: AuthDb) {
this.db = db;
}
login = async (user: User) => {
try {
userValidator(user);
const { username, password } = user;
const originalUser = await this.db.getUser(username);
await checkPassword(password, originalUser.password);
const token = createToken(originalUser.id);
return ResourceRetrieved({ token });
} catch (err) {
return errorHandler(err);
}
};
register = async (user: User) => {
try {
userValidator(user);
const userToCreate = await getHashedUser(user);
await this.db.createUser(userToCreate);
return ResourceCreated('User');
} catch (err) {
return errorHandler(err);
}
};
}
Each method receives the input, and runs validation logic that is outside of the Controller class, each of them can throw a predefined error object that are handled by the errorHandler() function, the Controller class just executes the steps of the action, but it doesn't know what kind of errors can happen.
ErrorHandler method
export default function errorHandler(err: ApiResponse) {
// Checks if err is not an Api Response
if (err.code == null || err.data == null) {
return InternalServerError;
}
return err;
}
It just checks for unexpected errors, returning a Internal Server Error if the error is not an ApiResponse object.
For example, the checkPassword method:
async function checkPassword(password: string, hash: string) {
const isPasswordRight = await compare(password, hash);
if (!isPasswordRight) {
throw Forbidden('Wrong password');
}
}
The Forbidden object code
const Forbidden = (msg: string) => ({
code: 403,
data: {
error: {
type: 'forbidden',
msg
}
}
});
The database is treated the same way
export default class AuthDb {
createUser = async (user: User) => {
try {
const { password, username } = user;
await query(createUserSQL, [username, password]);
return;
} catch (err) {
if ((err.code = '23505')) {
throw Forbidden('Username already in use');
}
throw InternalServerError;
}
};
getUser = async (username: string) => {
const { rows } = await query(getUserSQL, [username]);
if (rows.length === 0) {
throw ResourceNotFound('user');
}
return rows[0] as User;
};
}
Database errors are handled and re-trowed as a recognized error object rather than PostgreSQL default error objects so the controller method does not need to know what happens in the database class.
All errors are sent as responses and they all have the same pattern, with a status code and a data object.
The AuthHandler class is just a adapter for express and AuthController
export default class AuthHandler {
private controller: AuthController;
constructor(controller: AuthController) {
this.controller = controller;
}
login = async (req: Request, res: Response) => {
const { body } = req;
const result = await this.controller.login(body);
return handleResult(result, res);
}
register = async (req: Request, res: Response) => {
const { body } = req;
const result = await this.controller.register(body);
return handleResult(result, res);
}
}
It receives the output of controller's methods and returns a response regardless of being an error or a normal response.
Is is an usual solution for error handling?, i'm in doubt if it is easy to comprehend and maintainable.