Logo
⚠️ Unsaved
[M]:

Using Joi for API Input Validation

Hey there! If you're building APIs in Node.js, you've probably dealt with the headache of validating user inputs. This notebook walks through using Joi to handle that validation cleanly.

We'll cover validating URL parameters, query strings, headers, and request bodies - all the pieces you need to build robust APIs without writing tons of repetitive validation code. I'll show practical examples for each type of validation, including how to integrate with Express.js.

Let's get started!

[1]:
// Install Joi using npm
!npm install joi
$ npm install joi

added 6 packages in 3s

28 packages are looking for funding
  run `npm fund` for details
[2]:
// Import Joi
const Joi = require('joi');

// Check if Joi is loaded correctly
console.log("Joi version:", Joi.version, "\n");
Joi version: 17.13.3 
[M]:

1. Validating URL Parameters

URL parameters are common in RESTful APIs - things like /users/:userId or /products/:productId. Let's see how to make sure they're valid.

[3]:
// Schema for validating user ID parameter
const userIdParamSchema = Joi.object({
userId: Joi.alternatives().try(
Joi.number().integer().positive(),
Joi.string().guid({ version: 'uuidv4' })
).required()
});

// Example URL parameter validation function
function validateUserIdParam(params) {
const { error, value } = userIdParamSchema.validate(params);
if (error) {
return { isValid: false, error: error.details[0].message, params: null };
}
return { isValid: true, error: null, params: value };
}
[4]:
// Test with numeric ID
const numericParams = { userId: 123 };
console.log("Validating numeric user ID:\n");
console.log(validateUserIdParam(numericParams), "\n");

// Test with UUID
const uuidParams = { userId: '123e4567-e89b-12d3-a456-426614174000' };
console.log("Validating UUID user ID:\n");
console.log(validateUserIdParam(uuidParams), "\n");

// Test with invalid ID
const invalidParams = { userId: 'invalid-id' };
console.log("Validating invalid user ID:\n");
console.log(validateUserIdParam(invalidParams), "\n");
Validating numeric user ID:
{ isValid: true, error: null, params: { userId: 123 } } 
Validating UUID user ID:
{
  isValid: false,
  error: '"userId" must be a valid GUID',
  params: null
} 
Validating invalid user ID:
{
  isValid: false,
  error: '"userId" must be a valid GUID',
  params: null
} 
[M]:

2. Validating Query Parameters

Query parameters are those ?page=1&limit=20 things you see in URLs. They're super useful for filtering, sorting, and pagination.

[5]:
// Schema for validating query parameters in a product listing API
const productQuerySchema = Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(20),
sort: Joi.string().valid('price_asc', 'price_desc', 'newest', 'popular').default('newest'),
category: Joi.string().optional(),
minPrice: Joi.number().min(0).optional(),
maxPrice: Joi.number().greater(Joi.ref('minPrice')).optional(),
inStock: Joi.boolean().optional(),
q: Joi.string().max(100).optional()
});

// Example query parameter validation function
function validateProductQuery(query) {
const { error, value } = productQuerySchema.validate(query, {
stripUnknown: true, // Remove unknown parameters
abortEarly: false // Return all errors
});
if (error) {
return { isValid: false, error: error.details.map(d => d.message), query: null };
}
return { isValid: true, error: null, query: value };
}
[6]:
// Test with valid query parameters
const validQuery = {
page: 2,
limit: 50,
sort: 'price_asc',
category: 'electronics',
minPrice: 100,
maxPrice: 500,
inStock: true
};

console.log("Validating valid query parameters:\n");
console.log(validateProductQuery(validQuery), "\n");
Validating valid query parameters:
{
  isValid: true,
  error: null,
  query: {
    page: 2,
    limit: 50,
    sort: 'price_asc',
    category: 'electronics',
    minPrice: 100,
    maxPrice: 500,
    inStock: true
  }
} 
[7]:
// Test with invalid query parameters
const invalidQuery = {
page: 0, // Invalid: below minimum
limit: 200, // Invalid: above maximum
sort: 'invalid', // Invalid: not in allowed values
minPrice: 500,
maxPrice: 100, // Invalid: less than minPrice
unknown: 'value' // Unknown parameter (will be stripped)
};

console.log("Validating invalid query parameters:\n");
console.log(validateProductQuery(invalidQuery), "\n");
Validating invalid query parameters:
{
  isValid: false,
  error: [
    '"page" must be greater than or equal to 1',
    '"limit" must be less than or equal to 100',
    '"sort" must be one of [price_asc, price_desc, newest, popular]',
    '"maxPrice" must be greater than ref:minPrice'
  ],
  query: null
} 
[8]:
// Test with minimal query parameters (defaults applied)
const minimalQuery = {};

console.log("Validating minimal query parameters (with defaults):\n");
console.log(validateProductQuery(minimalQuery), "\n");
Validating minimal query parameters (with defaults):
{
  isValid: true,
  error: null,
  query: { page: 1, limit: 20, sort: 'newest' }
} 
[M]:

3. Validating Request Headers

Headers often contain important stuff like auth tokens and content types. Validating them is crucial for security.

[9]:
// Schema for validating API request headers
const headerSchema = Joi.object({
'authorization': Joi.string().pattern(/^Bearer /).required(),
'content-type': Joi.string().valid('application/json').required(),
'user-agent': Joi.string().required(),
'x-api-key': Joi.string().optional(),
'x-request-id': Joi.string().guid().optional()
}).unknown(true); // Allow other headers

// Example header validation function
function validateHeaders(headers) {
const { error, value } = headerSchema.validate(headers, { abortEarly: false });
if (error) {
return { isValid: false, error: error.details.map(d => d.message), headers: null };
}
return { isValid: true, error: null, headers: value };
}
[10]:
// Test with valid headers
const validHeaders = {
'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
'content-type': 'application/json',
'user-agent': 'Mozilla/5.0',
'x-api-key': 'api-key-123',
'accept-language': 'en-US'
};

console.log("Validating valid headers:\n");
console.log(validateHeaders(validHeaders), "\n");
Validating valid headers:
{
  isValid: true,
  error: null,
  headers: {
    authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
    'content-type': 'application/json',
    'user-agent': 'Mozilla/5.0',
    'x-api-key': 'api-key-123',
    'accept-language': 'en-US'
  }
} 
[11]:
// Test with invalid headers
const invalidHeaders = {
'authorization': 'Basic dXNlcjpwYXNz', // Invalid: not Bearer token
'content-type': 'text/plain', // Invalid: not application/json
'user-agent': 'Mozilla/5.0'
};

console.log("Validating invalid headers:\n");
console.log(validateHeaders(invalidHeaders), "\n");
Validating invalid headers:
{
  isValid: false,
  error: [
    '"authorization" with value "Basic dXNlcjpwYXNz" fails to match the required pattern: /^Bearer /',
    '"content-type" must be [application/json]'
  ],
  headers: null
} 
[M]:

4. Validating Request Bodies

Request bodies are where the bulk of your API data usually lives. They can get pretty complex, so good validation is essential.

[12]:
// Schema for validating a user creation request body
const createUserSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required()
.messages({
'string.pattern.base': 'Password must contain at least one uppercase letter, one lowercase letter, and one number'
}),
name: Joi.object({
first: Joi.string().min(1).max(50).required(),
last: Joi.string().min(1).max(50).required()
}).required(),
age: Joi.number().integer().min(13).required(),
bio: Joi.string().max(500).optional(),
roles: Joi.array().items(Joi.string().valid('user', 'admin', 'editor')).default(['user']),
settings: Joi.object({
newsletter: Joi.boolean().default(true),
theme: Joi.string().valid('light', 'dark').default('light'),
notifications: Joi.boolean().default(true)
}).default()
});
[13]:
// Example request body validation function
function validateCreateUserBody(body) {
const { error, value } = createUserSchema.validate(body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
return { isValid: false, error: error.details.map(d => d.message), body: null };
}
return { isValid: true, error: null, body: value };
}
[14]:
// Test with valid user creation body
const validUserBody = {
username: 'johndoe',
email: 'john@example.com',
password: 'Password123',
name: {
first: 'John',
last: 'Doe'
},
age: 30,
bio: 'Software developer with 5 years of experience',
roles: ['user', 'editor']
};

console.log("Validating valid user creation body:\n");
console.log(validateCreateUserBody(validUserBody), "\n");
Validating valid user creation body:
{
  isValid: true,
  error: null,
  body: {
    username: 'johndoe',
    email: 'john@example.com',
    password: 'Password123',
    name: { first: 'John', last: 'Doe' },
    age: 30,
    bio: 'Software developer with 5 years of experience',
    roles: [ 'user', 'editor' ],
    settings: { newsletter: true, theme: 'light', notifications: true }
  }
} 
[15]:
// Test with invalid user creation body
const invalidUserBody = {
username: 'j', // Invalid: too short
email: 'not-an-email', // Invalid: not an email
password: 'password', // Invalid: no uppercase or number
name: {
first: 'Jane'
// Missing last name
},
age: 10, // Invalid: below minimum age
roles: ['user', 'superadmin'] // Invalid: 'superadmin' not allowed
};

console.log("Validating invalid user creation body:\n");
console.log(validateCreateUserBody(invalidUserBody), "\n");
Validating invalid user creation body:
{
  isValid: false,
  error: [
    '"username" length must be at least 3 characters long',
    '"email" must be a valid email',
    'Password must contain at least one uppercase letter, one lowercase letter, and one number',
    '"name.last" is required',
    '"age" must be greater than or equal to 13',
    '"roles[1]" must be one of [user, admin, editor]'
  ],
  body: null
} 
[M]:

5. Express.js Integration

If you're using Express.js (and who isn't?), here's how to create reusable validation middleware.

[16]:
// Example Express.js validation middleware
function createValidationMiddleware(schema, type = 'body') {
return (req, res, next) => {
const { error, value } = schema.validate(req[type], {
abortEarly: false,
stripUnknown: type === 'body' // Only strip unknown fields from body
});
if (error) {
return res.status(400).json({
status: 'error',
message: 'Validation failed',
errors: error.details.map(err => ({
field: err.path.join('.'),
message: err.message
}))
});
}
// Replace request data with validated data
req[type] = value;
next();
};
}

// Example usage in Express routes (pseudocode)
console.log("Example Express route with Joi validation:\n");
console.log("app.post('/api/users', createValidationMiddleware(createUserSchema), (req, res) => {\n // Your handler code here\n // req.body is already validated\n});\n");
Example Express route with Joi validation:
app.post('/api/users', createValidationMiddleware(createUserSchema), (req, res) => {
  // Your handler code here
  // req.body is already validated
});
[17]:
// Example of validating multiple parts of a request
function validateRequest(schemas) {
return (req, res, next) => {
const validationErrors = {};
let hasErrors = false;
// Validate each part of the request
for (const [key, schema] of Object.entries(schemas)) {
if (!req[key]) continue;
const { error, value } = schema.validate(req[key], {
abortEarly: false,
stripUnknown: key === 'body'
});
if (error) {
hasErrors = true;
validationErrors[key] = error.details.map(err => ({
field: err.path.join('.'),
message: err.message
}));
} else {
req[key] = value;
}
}
if (hasErrors) {
return res.status(400).json({
status: 'error',
message: 'Validation failed',
errors: validationErrors
});
}
next();
};
Example Express route with multiple validations:
app.put('/api/users/:userId', validateRequest({
  params: userIdParamSchema,
  body: updateUserSchema,
  query: userQuerySchema
}), (req, res) => {
  // Your handler code here
});
[M]:

6. Complex Request Validation

Real-world APIs often need to validate complex requests with multiple components. Here's how to handle that.

[18]:
// Schema for validating a complete API request
const createProductRequestSchema = Joi.object({
// Body schema
body: Joi.object({
name: Joi.string().min(3).max(100).required(),
description: Joi.string().max(1000).optional(),
price: Joi.number().precision(2).positive().required(),
category: Joi.string().required(),
tags: Joi.array().items(Joi.string()).max(10).optional(),
attributes: Joi.object().pattern(
Joi.string(),
Joi.alternatives().try(Joi.string(), Joi.number(), Joi.boolean())
).max(20).optional(),
stock: Joi.number().integer().min(0).default(0),
images: Joi.array().items(
Joi.object({
url: Joi.string().uri().required(),
alt: Joi.string().max(100).optional(),
isPrimary: Joi.boolean().default(false)
})
).max(10).optional()
}).required(),
// Headers schema
headers: Joi.object({
'authorization': Joi.string().pattern(/^Bearer /).required(),
'content-type': Joi.string().valid('application/json').required()
}).unknown(true).required(),
// Query parameters schema
query: Joi.object({
dryRun: Joi.boolean().default(false)
}).optional()
});
[19]:
// Example complex request validation function
function validateCreateProductRequest(request) {
const { error, value } = createProductRequestSchema.validate(request, {
abortEarly: false
});
if (error) {
return { isValid: false, error: error.details.map(d => d.message), request: null };
}
return { isValid: true, error: null, request: value };
}
[20]:
// Test with valid complex request
const validComplexRequest = {
body: {
name: 'Wireless Headphones',
description: 'High-quality wireless headphones with noise cancellation',
price: 149.99,
category: 'electronics',
tags: ['wireless', 'audio', 'bluetooth'],
attributes: {
color: 'black',
batteryLife: 20,
waterproof: true
},
stock: 100,
images: [
{
url: 'https://example.com/images/headphones1.jpg',
alt: 'Front view',
isPrimary: true
},
{
url: 'https://example.com/images/headphones2.jpg',
alt: 'Side view'
}
]
},
headers: {
'authorization': 'Bearer token123',
'content-type': 'application/json',
'user-agent': 'Mozilla/5.0'
},
query: {
dryRun: true
}
};

Validating complex API request:
{
  isValid: true,
  error: null,
  request: {
    body: {
      name: 'Wireless Headphones',
      description: 'High-quality wireless headphones with noise cancellation',
      price: 149.99,
      category: 'electronics',
      tags: [Array],
      attributes: [Object],
      stock: 100,
      images: [Array]
    },
    headers: {
      authorization: 'Bearer token123',
      'content-type': 'application/json',
      'user-agent': 'Mozilla/5.0'
    },
    query: { dryRun: true }
  }
} 
[M]:

7. Reusable Schema Components

Don't repeat yourself! Create reusable schema components to keep your validation code DRY.

[21]:
// Example of reusable schema components
const schemas = {
id: Joi.alternatives().try(
Joi.number().integer().positive(),
Joi.string().guid({ version: 'uuidv4' })
),
pagination: Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(20)
}),
auth: Joi.object({
'authorization': Joi.string().pattern(/^Bearer /).required()
}).unknown(true),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)
.messages({
'string.pattern.base': 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'
})
};

// Example of using reusable components
const getUserSchema = Joi.object({
params: Joi.object({
userId: schemas.id.required()
}),
headers: schemas.auth,
query: schemas.pagination
});

console.log("Example of reusable schema components:\n");
console.log("getUserSchema structure:", JSON.stringify(getUserSchema.describe(), null, 2), "\n");
Example of reusable schema components:
getUserSchema structure: {
  "type": "object",
  "keys": {
    "params": {
      "type": "object",
      "keys": {
        "userId": {
          "type": "alternatives",
          "flags": {
            "presence": "required"
          },
          "matches": [
            {
              "schema": {
                "type": "number",
                "rules": [
                  {
                    "name": "integer"
                  },
                  {
                    "name": "sign",
                    "args": {
                      "sign": "positive"
                    }
                  }
                ]
              }
            },
            {
              "schema": {
                "type": "string",
                "rules": [
                  {
                    "name": "guid",
                    "args": {
                      "options": {
                        "version": "uuidv4"
                      }
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    },
    "headers": {
      "type": "object",
      "flags": {
        "unknown": true
      },
      "keys": {
        "authorization": {
          "type": "string",
          "flags": {
            "presence": "required"
          },
          "rules": [
            {
              "name": "pattern",
              "args": {
                "regex": "/^Bearer /"
              }
            }
          ]
        }
      }
    },
    "query": {
      "type": "object",
      "keys": {
        "page": {
          "type": "number",
          "flags": {
            "default": 1
          },
          "rules": [
            {
              "name": "integer"
            },
            {
              "name": "min",
              "args": {
                "limit": 1
              }
            }
          ]
        },
        "limit": {
          "type": "number",
          "flags": {
            "default": 20
          },
          "rules": [
            {
              "name": "integer"
            },
            {
              "name": "min",
              "args": {
                "limit": 1
              }
            },
            {
              "name": "max",
              "args": {
                "limit": 100
              }
            }
          ]
        }
      }
    }
  }
} 
[M]:

Wrapping Up

That's it! You now have a solid foundation for validating all kinds of API inputs with Joi. The examples here should cover most common scenarios you'll encounter when building APIs.

Remember that good validation not only prevents bugs and security issues but also provides better feedback to your API consumers. Clear error messages help developers understand what went wrong and how to fix it.

Happy coding!

Sign in to save your work and access it from anywhere