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:
ErrorRequestHandler
. Only used for the default validator.cors.CorsOptions | cors.CorsOptionsDelegate<cors.CorsRequest> | boolean
. (See cors){ json?: bodyParser.OptionsJson, urlencoded?: bodyParser.OptionsUrlencoded }
. (See body-parser){ 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:
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 flowOAuth2PasswordShape
: OAuth2 password grant flowExample 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:
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