@novice1/frame - v0.1.10

@novice1/frame

Web framework for building APIs.

Based on @novice1/app.

npm i @novice1/frame

Other dependencies:

npm i @novice1/routing @novice1/api-doc-generator
import { Frame } from '@novice1/frame'

const app = new Frame();

app.get({
path: '/',
name: 'Homepage',
description: 'API homepage',
tags: 'Index'
}, (_, res) => {
return res.json({ message: 'Hello world!' })
})

app.use((_, res) => {
return res.status(404).json({ message: 'Not found' });
})

app.listen(3000)

Based on @novice1/app framework configuration.

You can configure:

  • auth: (See @novice1/app)
  • middlewares: (See @novice1/app)
  • validators: (See @novice1/app. The default validator is @novice1/validator-joi if none was specified.)
  • validatorOnError: ErrorRequestHandler. Only used for the default validator.
  • cors: cors.CorsOptions | cors.CorsOptionsDelegate<cors.CorsRequest> | boolean. (See cors)
  • bodyParser: { json?: bodyParser.OptionsJson, urlencoded?: bodyParser.OptionsUrlencoded }. (See body-parser)
  • cookieParser: { options?: cookieParser.CookieParseOptions, secret?: string | string[] }. (See cookie-parser)
import { Frame } from '@novice1/frame'

const app = new Frame({
framework: {
cors: true,
validatorOnError: (err, _req, res) => {
// avoid sending back sensitive data ('_original' from joi validator)
const {_original, ...details} = err
return res.status(400).json({...details, code: 'badRequest'})
}
}
});

By default, the documentation of your API gets generated and accessible at:

  • /docs,
  • /docs/redoc,
  • /docs/schema,
  • /docs/schema?format=postman.

You can add more info to the documentation and change the /docs prefix to something else if you want.

import { Frame } from '@novice1/frame'

const app = new Frame({
docs: {
host: {
url: 'http://{domain}:{port}',
description: 'Dev API',
variables: {
domain: {
default: 'localhost',
enum: [
'localhost',
'127.0.0.1'
],
description: 'Dev domain'
},
port: {
default: '3000'
}
}
},
title: '@novice1/frame API',
license: {
name: 'ISC',
url: 'https://opensource.org/license/isc-license-txt'
},
options: {
logo: {
url: 'https://path-to-image.png',
alt: 'Dev API logo'
}
}
}
});

You can shape your frame (@novice1/frame) to complete and easily give authenticity to your API.

Shapes can help you:

  • make an authorization flow (OAuth2, ...),
  • generate a detailed documentation,
  • ...

The security shapes implement the interface ISecurityShape. @novice1/frame offers the following security shapes:

  • OAuth2ACShape: OAuth2 authorization code grant flow (+ PKCE)
  • OAuth2ClientCredsShape: OAuth2 client credentials grant flow
  • OAuth2PasswordShape: OAuth2 password grant flow

Example of using OAuth2 password grant flow:

import { 
Frame,
OAuth2PasswordShape,
OAuth2PasswordTokenRoute,
OAuth2TokenResponse
} from '@novice1/frame'

const tokenRoute = new OAuth2PasswordTokenRoute('/oauth2/v1/token')
.setHandler((params, _req, res) => {
let token: string;
let refreshToken: string;

// Here you handle the access token request
// ...

return res.json(
new OAuth2TokenResponse(token, 'bearer')
.setExpiresIn(3600)
.setRefreshToken(refreshToken)
.setScope(params.scope)
)
})

const securityShape = new OAuth2PasswordShape('oAuth2', tokenRoute)
.setDescription('This API uses OAuth 2 with the password grant flow. [More info](/docs/redoc)')
.clientAuthenticationToBody()
.setAuthHandlers(
(req, res, next) => {
const authHeaderValue = req.header('authorization')
if (authHeaderValue && authHeaderValue.startsWith('Bearer ')) {
const token = authHeaderValue.substring(7)

// Here you validate the token
// ...

// authorized to go further
return next()
}

return res.status(401).json({
error: 'unauthorized'
})
}
)

const app = new Frame({
security: securityShape
});

Security shapes:

  • generate the documentation for the authorization flow(s),
  • register route(s) and middleware(s) to the frame.

That way, you just have to go to /docs to try the authorization flow for your API. Easy, right?

You could even make your own security shape:

import { 
Frame,
ApiKeyLocation,
ApiKeyUtil,
GroupAuthUtil,
ISecurityShape
} from '@novice1/frame'
import routing from '@novice1/routing'

class MySecurityShape implements ISecurityShape {
/**
* Optional: define the routes that generate the API key
*/
//router(): routing.IRouter {
// return routing()
//}

/**
* @returns a BaseAuthUtil extension ([@novice1/api-doc-generator](https://kisiwu.github.io/novice-api-doc-generator/latest/classes/utils_auth_basicAuthUtil.BasicAuthUtil.html))
*/
scheme(): GroupAuthUtil {
const apiKey = new ApiKeyUtil('APIKey')
.setApiKeyLocation(ApiKeyLocation.header) // the location of the key (cookie, header or query)
.setName('authorization') // the header's name
.setDescription('Generated API key from your application console. MUST be prefixed by \'Session \'.')

return new GroupAuthUtil([
apiKey
])
}

/**
* @returns Middlewares that give authorization
*/
authHandlers(): routing.RequestHandler[] {
return [
(req, res, next) => {
const value = req.header('authorization')

if (value?.startsWith('Session ')) {
return res.status(401).json({ error: 'unauthorized' })
}

// Here you validate the api key
// ...

// authorized to go further
return next()
}
]
}
}

const app = new Frame({
security: new MySecurityShape()
});

Using a security shape is the recommended way to register an authorization flow to the frame. Of course, there are other ways but that would be using @novice1/app and @novice1/api-doc-generator without the advantages of the Frame.

We saw previously that you could configure the documentation. Tools, including the class DocsShape, can help you with that.

The current list of those tools is:

  • DocsShape (of course)
  • ExampleShape
  • MediaTypeShape
  • SchemaShape
  • ContextResponseShape
  • GroupResponseShape
  • ResponseShape

If you are familiar with @novice1/api-doc-generator (which is not possible for any human being, lol), you will understand their purpose faster than anyone else.

import { DocsShape, Frame } from '@novice1/frame'

const docs = new DocsShape()
.setHost({
url: 'http://{domain}:{port}',
description: 'Dev API',
variables: {
domain: {
default: 'localhost',
enum: [
'localhost',
'127.0.0.1'
],
description: 'Dev domain'
},
port: {
default: '3000'
}
}
})
.setTitle('@novice1/frame API')
.setLicense('ISC', 'https://opensource.org/license/isc-license-txt')
.setLogo('https://path-to-image.png', 'Dev API logo')

const app = new Frame({ docs });

Here is an example that we can start right away. It uses ResponseShape, MediaTypeShape, GroupResponseShape and ContextResponseShape. Those classes extend respectively ResponseUtil, MediaTypeUtil, GroupResponseUtil and ContextResponseUtil from @novice1/api-doc-generator.

import { 
ContextResponseShape,
DocsShape,
Frame,
GroupResponseShape,
MediaTypeShape,
ResponseShape
} from '@novice1/frame';
import Joi from 'joi'

// response shapes

const greetingsResponse = new ResponseShape('GreetingsResponse')
.setDescription('Success')
.addMediaType('application/json', new MediaTypeShape({
examples: {
default_output: {
value: { message: 'Hello world!' },
description: 'The default output.'
},
custom_output: {
value: { message: 'Hello novice!' },
description: 'The custom output.'
}
},
schema: {
description: 'A message object.',
type: 'object',
properties: {
message: {
description: 'The message content.',
type: 'string',
example: 'Hello world!'
}
},
required: [
'message'
]
}
}))

const badRequestResponse = new ResponseShape('ValidatorBadRequest')
.setDescription('Bad request')
.addMediaType('application/json', new MediaTypeShape({
schema: {
type: 'object',
properties: {
details: {
type: 'array'
}
}
}
}))


// frame

const frame = new Frame({
docs: new DocsShape()
.setResponses(
new GroupResponseShape([
greetingsResponse,
badRequestResponse
])
)
})


// routes

frame.get({
path: '/greetings',
description: 'Greetings',
tags: 'Tests',
parameters: {
query: {
name: Joi.string()
.description('Your name.')
.example('novice')
.min(2)
.invalid('Frank')
}
},
responses: new GroupResponseShape([
new ContextResponseShape(greetingsResponse)
.setCode(200),
new ContextResponseShape(badRequestResponse)
.setCode(400)
])
}, (req, res) => {
return res.json({ message: `Hello ${req.query.name ? req.query.name : 'world'}!` })
})


// listen on port 3000

frame.listen(3000)

Running the example above locally, we can see that the documentation (at localhost:3000/docs/ or localhost:3000/docs/redoc) defines response samples for the route /greetings.

Now let's use ExampleShape and SchemaShape to get the following:

import {
ContextResponseShape,
DocsShape,
ExampleShape,
Frame,
GroupResponseShape,
MediaTypeShape,
ResponseShape,
SchemaShape
} from '@novice1/frame';
import Joi from 'joi'


// example shapes

const defaultGreetingsResponse = new ExampleShape('DefaultGreetingsResponse')
.setValue({ message: 'Hello world!' })
.setDescription('The default output.')

const customGreetingsResponse = new ExampleShape('CustomGreetingsResponse')
.setValue({ message: 'Hello novice!' })
.setDescription('The custom output.')


// schema shapes

const messageSchema = new SchemaShape('Message', {
description: 'The message content.',
type: 'string',
example: 'A simple message.'
})

const messageResponseSchema = new SchemaShape('MessageResponse', {
description: 'A message object.',
type: 'object',
properties: {
message: messageSchema
},
required: [
'message'
]
})


// response shapes

const greetingsResponse = new ResponseShape('GreetingsResponse')
.setDescription('Success')
.addMediaType('application/json', new MediaTypeShape({
examples: {
default_output: defaultGreetingsResponse,
custom_output: customGreetingsResponse
},
schema: messageResponseSchema
}))

const badRequestResponse = new ResponseShape('ValidatorBadRequest')
.setDescription('Bad request')
.addMediaType('application/json', new MediaTypeShape({
schema: {
type: 'object',
properties: {
details: {
type: 'array'
}
}
}
}))


// frame

const frame = new Frame({
docs: new DocsShape()
.setExamples([
defaultGreetingsResponse,
customGreetingsResponse
])
.setSchemas([
messageSchema,
messageResponseSchema
])
.setResponses(
new GroupResponseShape([
greetingsResponse,
badRequestResponse
])
)
})

// ...

From the example above, we can understand that ExampleShape and SchemaShape help us create and reuse examples and schemas.

If you know that you are going to use the same responses, schemas or examples multiple times, create shapes for them. The benefits of using those shapes are:

But of course to make it even shorter, you don't have to define responses at all 😉. /s