Categories
Fastify

Server-Side Development with Fastify — Fluent Schema Request Validation

Fastify is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Fastify.

Fluent Schema

We can set up a schema to validate request content with the fluent-schema module in our Fastify app.

For example, we can write:

const fastify = require('fastify')({})
const S = require('fluent-schema')

const MY_KEYS = {
  KEY1: 'ONE',
  KEY2: 'TWO'
}

const bodyJsonSchema = S.object()
  .prop('someKey', S.string())
  .prop('someOtherKey', S.number())
  .prop('requiredKey', S.array().maxItems(3).items(S.integer()).required())
  .prop('nullableKey', S.mixed([S.TYPES.NUMBER, S.TYPES.NULL]))
  .prop('multipleTypesKey', S.mixed([S.TYPES.BOOLEAN, S.TYPES.NUMBER]))
  .prop('multipleRestrictedTypesKey', S.oneOf([S.string().maxLength(5), S.number().minimum(10)]))
  .prop('enumKey', S.enum(Object.values(MY_KEYS)))
  .prop('notTypeKey', S.not(S.array()))

const queryStringJsonSchema = S.object()
  .prop('name', S.string())
  .prop('excitement', S.integer())

const paramsJsonSchema = S.object()
  .prop('par1', S.string())
  .prop('par2', S.integer())

const headersJsonSchema = S.object()
  .prop('x-foo', S.string().required())

const schema = {
  body: bodyJsonSchema,
  querystring: queryStringJsonSchema,
  params: paramsJsonSchema,
  headers: headersJsonSchema
}

fastify.post('/', { schema }, function (req, reply) {
  reply.send('success')
})
const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

We add the bodyJsonSchema to validate the request body.

S.object is called to let us validate objects.

prop is used to validate properties. It takes the property name as the first argument.

S.string indicates that it’s a string.

S.array validates that a property is an array.

maxItems validates the max number of items in an array.

S.oneOf lets us specify one of multiple types for a property.

S.enum validates a property is an enum. It takes a string array with the enum values.

We can also specify validation schemas for query strings, URL parameters, and headers with fluent-schema .

Now when we make a request with content that doesn’t match the schema, we’ll get an error.

Schema Reuse

We can reuse our schemas by adding an ID to them with the id method.

For instance, we can write:

const fastify = require('fastify')({})
const S = require('fluent-schema')

const addressSchema = S.object()
  .id('#address')
  .prop('line1').required()
  .prop('line2')
  .prop('country').required()
  .prop('city').required()
  .prop('zip').required()

const commonSchemas = S.object()
  .id('app')
  .definition('addressSchema', addressSchema)

fastify.addSchema(commonSchemas)

const bodyJsonSchema = S.object()
  .prop('home', S.ref('app#address')).required()
  .prop('office', S.ref('app#/definitions/addressSchema')).required()

const schema = { body: bodyJsonSchema }

fastify.post('/', { schema }, function (req, reply) {
  reply.send('success')
})
const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

to create our schemas with the addressSchema .

Then we add the definition to a reusable schema with the definition method.

id specifies the ID of the schema.

S.ref lets us reference a schema in the code by its ID so we can reuse them.

Then we create the schema object and pass it to the 2nd argument of post .

We can also create the schema with an object:

const fastify = require('fastify')({})

const sharedAddressSchema = {
  $id: 'sharedAddress',
  type: 'object',
  required: ['line1', 'country', 'city', 'zip'],
  properties: {
    line1: { type: 'string' },
    line2: { type: 'string' },
    country: { type: 'string' },
    city: { type: 'string' },
    zip: { type: 'string' }
  }
}

fastify.addSchema(sharedAddressSchema)

const bodyJsonSchema = {
  type: 'object',
  properties: {
    vacation: 'sharedAddress#'
  }
}

const schema = { body: bodyJsonSchema }

fastify.post('/', { schema }, function(req, reply) {
  reply.send('success')
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

We create an object for the schema.

Then we all fastify.addSchema to add the schema.

And then we create the schema object with the added schema which references the schema by the $id property.

And then we add the schema to the route handler.

Conclusion

We can add validation for requests with the fluent-schema library to our Fastify app.

Categories
Fastify

Server-Side Development with Fastify — Request Validation

Fastify is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Fastify.

Request Validation

We can specify how to validate requests with schema objects.

For example, we can write:

const fastify = require('fastify')({})

const bodyJsonSchema = {
  type: 'object',
  required: ['requiredKey'],
  properties: {
    someKey: { type: 'string' },
    someOtherKey: { type: 'number' },
    requiredKey: {
      type: 'array',
      maxItems: 3,
      items: { type: 'integer' }
    },
    nullableKey: { type: ['number', 'null'] },
    multipleTypesKey: { type: ['boolean', 'number'] },
    multipleRestrictedTypesKey: {
      oneOf: [
        { type: 'string', maxLength: 5 },
        { type: 'number', minimum: 10 }
      ]
    },
    enumKey: {
      type: 'string',
      enum: ['John', 'Foo']
    },
    notTypeKey: {
      not: { type: 'array' }
    }
  }
}

const queryStringJsonSchema = {
  type: 'object',
  properties: {
    name: { type: 'string' },
    excitement: { type: 'integer' }
  }
}

const paramsJsonSchema = {
  type: 'object',
  properties: {
    par1: { type: 'string' },
    par2: { type: 'number' }
  }
}

const headersJsonSchema = {
  type: 'object',
  properties: {
    'x-foo': { type: 'string' }
  },
  required: ['x-foo']
}

const schema = {
  body: bodyJsonSchema,
  querystring: queryStringJsonSchema,
  params: paramsJsonSchema,
  headers: headersJsonSchema
}

fastify.post('/', { schema }, (request, reply)=>{
  reply.send('success')
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

to add validation schemas for the request body, header, query strings, and URL parameters.

bodyJsonSchema has the schema for the body.

The type property specifies its type.

required specifies which fields are required.

properties specifies the properties that the request body can have.

The type in the properties specifies the type.

queryStringJsonSchema specifies what can be included in the query string.

We specify what can be included in the properties property.

paramsJsonSchema is the object for validating URL parameters.

And headersJsonSchema specifies the schema for the request headers.

We pass all of them into the schema object.

And then we pass that into the 2nd argument of fastify.post to let us validate with them.

Ajv Plugins

We can use the Ajv plugin to validate requests.

For instance, we can write:

const fastify = require('fastify')({
  ajv: {
    plugins: [
      require('ajv-merge-patch')
    ]
  }
})

fastify.post('/', {
  handler (req, reply) { reply.send({ ok: 1 }) },
  schema: {
    body: {
      $patch: {
        source: {
          type: 'object',
          properties: {
            q: {
              type: 'string'
            }
          }
        },
        with: [
          {
            op: 'add',
            path: '/properties/q',
            value: { type: 'number' }
          }
        ]
      }
    }
  }
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

to add validation with the ajv-merge-patch plugin.

We add the plugin with:

const fastify = require('fastify')({
  ajv: {
    plugins: [
      require('ajv-merge-patch')
    ]
  }
})

Then we add the schema object to add the schema.

The source specifies the request body structure with the data types for the properties.

And the with property lets us add more options for validation.

The op is the operation to do with the existing schema.

'add' lets us add more requirements.

path is the property path of the body to validate.

value.type is the data type we want the properties to be.

Conclusion

We can add request validation with Fastify.

Categories
Fastify

Server-Side Development with Fastify — Response Serialization and Request Validation Error Handling

Fastify is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Fastify.

Serialization

We can change the way that responses are serialized.

To do this, we write:

const fastify = require('fastify')({})

const schema = {
  response: {
    '2xx': {
      type: 'object',
      properties: {
        value: { type: 'string' },
        otherValue: { type: 'boolean' }
      }
    },
    201: {
      value: { type: 'string' }
    }
  }
}

fastify.get('/', { schema }, (request, reply) => {
  reply.send({ value: 1, otherValue: 'foo' })
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

We add the response schema with the schema.response property.

We specify the response data type and structure for each status code.

The type specifies the data type of the response.

properties specifies the data type for the properties in the response.

In the GET / request handler, we call reply.send with an object with the properties listed in the schema.

And the properties will automatically be converted to the types specified in the schema.

So we get:

{"value":"1","otherValue":true}

when we make a GET request to / .

We can also specify the response schema in the request.

To do this, we write:

const fastify = require('fastify')({})

fastify.setSerializerCompiler(({ schema, method, url, httpStatus }) => {
  return data => JSON.stringify(data)
})

fastify.get('/', {
  handler (req, reply) {
    reply.send({ id: 1, name: 'Foo', image: 'BIG IMAGE' })
  },
  schema: {
    response: {
      '2xx': {
        id: { type: 'number' },
        name: { type: 'string' }
      }
    }
  }
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

We call fastify.setSerializerCompiler to compile the response.

And we add the schema to the fastify.get method to specify the response schema.

Error Handling

We can change how errors are handled when validation fails.

For example, we can write:

const fastify = require('fastify')({})

const schema = {
  body: {
    type: 'object',
    properties: {
      name: { type: 'string' }
    },
    required: ['name']
  }
}

fastify.post('/', { schema, attachValidation: true }, function (req, reply) {
  if (req.validationError) {
    reply.code(400).send(req.validationError)
  }
  else {
    reply.send('success')
  }
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

We specify the validation schema.

And in the fastify.post method, we pass in an object with the schema and attachValidation set to true .

We check if there are any validation errors with the req.validationError property.

And we get the validation errors with the req.validationError property.

Conclusion

We can change how responses are serialized and how errors are handled with Fastify.

Categories
Fastify

Server-Side Development with Fastify — Request Validation with External Libraries

Fastify is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Fastify.

Ajv Validation Options

We can add validation options with the fastify.setValidatorCompiler method.

For example, we can write:

const fastify = require('fastify')({
  ajv: {
    plugins: [
      require('ajv-merge-patch')
    ]
  }
})

const Ajv = require('ajv')
const ajv = new Ajv({
  removeAdditional: true,
  useDefaults: true,
  coerceTypes: true,
  nullable: true,
})
fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {
  return ajv.compile(schema)
})

fastify.post('/', {
  handler (req, reply) { reply.send({ ok: 1 }) },
  schema: {
    body: {
      $patch: {
        source: {
          type: 'object',
          properties: {
            q: {
              type: 'string'
            }
          }
        },
        with: [
          {
            op: 'add',
            path: '/properties/q',
            value: { type: 'number' }
          }
        ]
      }
    }
  }
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

We call the Ajv constructor and call the serValidationCompiler method with it to add the options from the object we pass into the Ajv constructor.

We can validate options with other libraries.

For example, we can use the @hapi/joi component.

To use it, we write:

const fastify = require('fastify')({})
const Joi = require('@hapi/joi')

fastify.post('/', {
  schema: {
    body: Joi.object().keys({
      hello: Joi.string().required()
    }).required()
  },
  validatorCompiler: ({ schema, method, url, httpPart }) => {
    return data => schema.validate(data)
  }
}, (request, reply) => {
  reply.send('hello')
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

to add the module.

We call the Joi.object method to make sure the request body is an object.

And we call required to make it required.

Also, we add the validatorCompiler method to add a function to connect the schema to the route.

Also, we can use the yup library to add validator for requests.

For instance, we can write:

const fastify = require('fastify')({})
const yup = require('yup')
const yupOptions = {
  strict: false,
  abortEarly: false,
  stripUnknown: true,
  recursive: true
}

fastify.post('/', {
  schema: {
    body: yup.object({
      age: yup.number().integer().required(),
      sub: yup.object().shape({
        name: yup.string().required()
      }).required()
    })
  },
  validatorCompiler: ({ schema, method, url, httpPart }) => {
    return (data) => {
      try {
        const result = schema.validateSync(data, yupOptions)
        return { value: result }
      } catch (e) {
        return { error: e }
      }
    }
  }
}, (request, reply) => {
  reply.send('hello')
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

to add validation for the request body.

We call yup.number().integer().required() to make a required number field.

And we call yup.object().shape() to validate objects.

And we call yup.string().required() to add a required string field.

Conclusion

We can add validations for requests with external libraries with Fastify apps.

Categories
Fastify

Server-Side Development with Fastify — Decorator Getters and Setters and Validation

Fastify is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Fastify.

Decorator Getters and Setters

We can add getters and setters to decorators.

For example, we can write:

const fastify = require('fastify')({})

fastify.decorate('foo', {
  getter () {
    return 'a getter'
  }
})

fastify.get('/', async function (request, reply) {
  reply.send({hello: fastify.foo})
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

Then fastify.foo returns 'a getter' .

So we get:

{"hello":"a getter"}

as the response of the / route.

Validation

We can add validation for requests.

For example, we can write:

const fastify = require('fastify')({})

fastify.addSchema({
  $id: 'http://example.com/',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

fastify.post('/', {
  handler (request, reply) {
    reply.send('hello')
  },
  schema: {
    body: {
      type: 'array',
      items: { $ref: 'http://example.com#/properties/hello' }
    }
  }
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

We call fastify.addSchema to add the schema.

We specify that it is a string with the properties property.

Then we reference it in the object in the body.

body specifies the body structure.

We have the items property with the $ref property that references the properties.hello property to get the data type of the item of the array.

So when we make a POST request to the / route with a body like:

[
   "abc"
]

we see 'hello' in the response.

Otherwise, we get a 400 error.

$ref can also be used as a root reference.

For example, we can write:

const fastify = require('fastify')({})

fastify.addSchema({
  $id: 'commonSchema',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

fastify.post('/', {
  handler (request, reply) {
    reply.send('hello')
  },
  schema: {
    body: { $ref: 'commonSchema#' },
    headers: { $ref: 'commonSchema#' }
  }
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

Then when we send an object with the request body, we, get the 'hello' response.

Retrieving Shared Schemas

We can get a shared schema.

For example, we can write:

const fastify = require('fastify')({})

fastify.addSchema({
  $id: 'schemaId',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

const mySchemas = fastify.getSchemas()
const mySchema = fastify.getSchema('schemaId')
console.log(mySchemas, mySchema)

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

We call addSchema to add our schema.

Then we can get the schemas with the getSchemas method.

The getSchema method gets the schema with the given ID.

We can also use that within the select scope.

For instance, we can write:

const fastify = require('fastify')({})

fastify.addSchema({ $id: 'one', my: 'hello' })

fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })

fastify.register((instance, opts, done) => {
  instance.addSchema({ $id: 'two', my: 'ciao' })
  instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) })

  instance.register((subinstance, opts, done) => {
    subinstance.addSchema({ $id: 'three', my: 'hola' })
    subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) })
    done()
  })
  done()
})

const start = async () => {
  try {
    await fastify.listen(3000, '0.0.0.0')
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

Then we call addSchema to add the schema inside the given context.

It’ll only be available within the given context.

getSchemas also get the schemas within the given context.

So when we make a request to the / route, we get:

{"one":{"$id":"one","my":"hello"}}

as the response.

Conclusion

We can add decorators with getters and setters, and we can add request validation schemas with Fastify.