Hapi, Joi, and TypeScript: A Comprehensive Guide
In the world of Node.js development, building robust and reliable web applications often requires a combination of powerful frameworks and tools. Hapi, Joi, and TypeScript are three such technologies that, when used together, can significantly enhance the development process. Hapi is a rich framework for building applications and services. It allows developers to focus on writing reusable application logic instead of spending time building infrastructure. Joi, on the other hand, is a powerful schema description language and data validator for JavaScript. It helps in validating user input, ensuring that the data received by your application is in the expected format. TypeScript is a superset of JavaScript that adds static typing to the language, making it more scalable and maintainable, especially in large projects. In this blog post, we will explore the fundamental concepts of Hapi, Joi, and TypeScript, learn how to use them together, discuss common practices, and highlight some best practices.
Table of Contents#
Fundamental Concepts#
Hapi#
Hapi is a Node.js framework that provides a simple and powerful way to build web applications and APIs. It has a rich set of features such as routing, input validation, caching, and authentication. Hapi follows a plugin-based architecture, which means that most of its functionality is provided through plugins. This makes it highly customizable and extensible.
Here is a simple example of a Hapi server:
import * as Hapi from '@hapi/hapi';
const init = async () => {
const server = Hapi.server({
port: 3000,
host: 'localhost'
});
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
return 'Hello, World!';
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();Joi#
Joi is a validation library for JavaScript. It allows you to define schemas for your data and then validate input against those schemas. Joi schemas are highly customizable and can be used to validate various types of data, such as strings, numbers, arrays, and objects.
Here is an example of using Joi to validate an object:
import * as Joi from 'joi';
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required()
});
const user = {
username: 'john_doe',
password: 'password123'
};
const { error, value } = schema.validate(user);
if (error) {
console.log(error.details[0].message);
} else {
console.log('Valid input');
}TypeScript#
TypeScript is a superset of JavaScript that adds static typing to the language. It helps in catching errors early in the development process and makes the code more understandable and maintainable. TypeScript code is transpiled to plain JavaScript code, which can be run in any JavaScript environment.
Here is a simple example of a TypeScript function:
function add(a: number, b: number): number {
return a + b;
}
const result = add(5, 3);
console.log(result);Usage Methods#
Setting up a Hapi project with TypeScript#
To set up a Hapi project with TypeScript, follow these steps:
- Create a new directory for your project and navigate to it:
mkdir hapi-joi-typescript-project
cd hapi-joi-typescript-project- Initialize a new Node.js project:
npm init -y- Install the necessary dependencies:
npm install @hapi/hapi typescript @types/node --save- Create a
tsconfig.jsonfile in the root of your project:
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}- Create a
srcdirectory and aserver.tsfile inside it:
mkdir src
touch src/server.ts-
Write your Hapi server code in
server.ts(as shown in the previous Hapi example). -
Transpile your TypeScript code to JavaScript:
npx tsc- Run the generated JavaScript code:
node dist/server.jsUsing Joi for input validation in Hapi with TypeScript#
To use Joi for input validation in a Hapi application with TypeScript, you can define a validation schema for each route and use it in the route configuration.
Here is an example:
import * as Hapi from '@hapi/hapi';
import * as Joi from 'joi';
const init = async () => {
const server = Hapi.server({
port: 3000,
host: 'localhost'
});
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required()
});
server.route({
method: 'POST',
path: '/users',
options: {
validate: {
payload: userSchema
}
},
handler: (request, h) => {
const user = request.payload;
return `User ${user.username} created successfully`;
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();Common Practices#
Error handling#
When using Hapi, Joi, and TypeScript, proper error handling is crucial. In Hapi, you can use the onPreResponse event to handle errors globally.
Here is an example:
import * as Hapi from '@hapi/hapi';
const init = async () => {
const server = Hapi.server({
port: 3000,
host: 'localhost'
});
server.ext('onPreResponse', (request, h) => {
const response = request.response;
if (response.isBoom) {
const error = response;
return h.response({
statusCode: error.output.statusCode,
error: error.output.payload.error,
message: error.message
}).code(error.output.statusCode);
}
return h.continue;
});
server.route({
method: 'GET',
path: '/error',
handler: () => {
throw new Error('Something went wrong');
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();Route organization#
As your application grows, it's important to organize your routes in a modular way. You can create separate files for different sets of routes and then register them with the Hapi server.
Here is an example:
// routes/users.ts
import * as Hapi from '@hapi/hapi';
const userRoutes: Hapi.ServerRoute[] = [
{
method: 'GET',
path: '/users',
handler: (request, h) => {
return 'List of users';
}
},
{
method: 'POST',
path: '/users',
handler: (request, h) => {
return 'User created';
}
}
];
export default userRoutes;
// server.ts
import * as Hapi from '@hapi/hapi';
import userRoutes from './routes/users';
const init = async () => {
const server = Hapi.server({
port: 3000,
host: 'localhost'
});
server.route(userRoutes);
await server.start();
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();Best Practices#
Code modularity#
Code modularity is essential for maintaining a large and complex application. You can break your code into smaller, reusable modules. For example, you can create separate modules for database access, authentication, and business logic.
Testing#
Testing is an important part of the development process. You can use testing frameworks like Jest or Mocha to write unit tests for your Hapi routes and Joi validation schemas.
Here is an example of a unit test for a Hapi route using Jest:
import * as Hapi from '@hapi/hapi';
import userRoutes from './routes/users';
describe('User routes', () => {
let server: Hapi.Server;
beforeAll(async () => {
server = Hapi.server({
port: 3000,
host: 'localhost'
});
server.route(userRoutes);
});
test('GET /users should return a list of users', async () => {
const response = await server.inject({
method: 'GET',
url: '/users'
});
expect(response.statusCode).toBe(200);
expect(response.payload).toBe('List of users');
});
});Conclusion#
In this blog post, we have explored the fundamental concepts of Hapi, Joi, and TypeScript, learned how to use them together, discussed common practices, and highlighted some best practices. By combining these technologies, you can build robust, scalable, and maintainable web applications and APIs. Remember to follow the best practices and write clean, modular code to make your development process more efficient.