@harlos/nana
v0.1.1
Published
A minimal, type-safe Node.js backend framework
Maintainers
Readme
Nana
A minimal, type-safe Node.js backend framework built on Express with composable middleware and context flow.
import { NanaServer } from '@harlos/nana'
const server = new NanaServer({ port: 3000 }) // default port is 7777
server.get<{ message: string }>('/hello', () => ({ message: 'Hello World!' }))
server.run()Features
- Based on Express - Leverages the power of Express.js
- Everything in Single Context - Grab query, params, body, and more from a single context object (or add your own)
- Type-Safe - Type everything
- Custom Action, Transformer, and Error Handler - Organize and unify how responses are sent
Installation
npm install @harlos/nana
# or
yarn add @harlos/nana
# or
pnpm add @harlos/nanaCore Concepts
Basic usage
import { NanaServer, NanaRouter } from '@harlos/nana'
const server = new NanaServer() // extends NanaRouter
// Create sub-routers directly
const apiRouter = server.use('/api')
// or you can create router first
const userRouter = new NanaRouter()
userRouter.get('/', () => ({ userId: 1, name: 'Harlos' }))
// Then mount it
apiRouter.use('/users', userRouter)
server.run()Middleware - everything in Context
// A NanaMiddleware adds new context to current request
const authMiddleware = new NanaMiddleware<{ userId: number }>(
async ({ req }) => {
const token = req.headers.authorization
const userId = await validateToken(token)
return { userId } // this saves to req.ctx under the hood
}
)
// for now you need to type the context for the router
// this seems verbose, but it helps when you have multiple middlewares
const apiRouter = server.use<{ userId: number }>('/api')
apiRouter.use(authMiddleware)
// get userId directly from context
// also you can type the controller
apiRouter.get<{ profile: Profile }>('/profile', ({ userId }) => {
return { profile: await getProfile(userId) }
})
// default available context includes (be aware of overriding):
type CTXArgument<CustomCTX> = {
...CustomCTX, // additional context from middleware
...req.query, // auto expanded query parameters
...req.params, // auto expanded route parameters
body: req.body as any, // request body
req, // Express Request object
res, // Express Response object
}NanaRouter - Action & Transformer & Error Handler
// everything will be awaited
const router = new NanaRouter<{ message: string }>(
action?: (data, ctx) => void | Promise<void>,
transformer?: (raw, ctx) => ResponseData | Promise<ResponseData>,
errorHandler?: (err, ctx, errorLogger?) => void | Promise<void>,
)
// Action is the response handler
// By default, the action sends the data returned by the handler with 200 status code
server.get('/data', ({ res }) => ({ message: 'Hello World' }))
// This sends { message: 'Hello World' } with 200 status code
// You can also use a custom action
const customAction = (data, { res }) => { res.status(201).send(data) }
const customRedirectAction = (url, { res }) => { res.redirect(url) }
// actions can be overridden in nested routers
// remember to send the response, otherwise it will hang!
// Transformer is used to transform the response data
// By default, it just returns the data as is
// You can also use a custom transformer
const customTransformer = (data, { req }) => {
// Transform data based on request
return { data, id: req.id }
}
// transformers will be wrapped recursively for nested routers
// e.g. finalData = transformerC(transformerB(transformerA(data, ctx), ctx), ctx) // route A/B/C
server.get('/error', () => {
// throw a NanaError to return a specific HTTP error
throw new NanaError(400, 'Something went wrong')
// if somehow other error occurs, default error handler automatically sends a 500 response
})
// of course, you can implement your own error handler
// error handler can be overridden in nested routers
// remember to send the response, otherwise it will hang!Extending Nana classes
I will not cover this in detail (yet), but you can extend Nana into your own classes.
API Reference
NanaServer
const server = new NanaServer({
port?: number, // Default: 7777
onStart?: () => void, // Callback when server starts
})
server.run() // Start the server
// inherits NanaRouter methods
const router = server.use(path) // create sub-router
server.use(path, router) // mount sub-router
server.use(middleware) // Add middleware
server.get(route, handler) // HTTP methods
server.post(route, handler)NanaMiddleware
// these can also be promises
const middleware = new NanaMiddleware<ContextType>(
contextCreator, // (currentContext) => addedContext | void
errorHandler?, // middleware specific, same behavior as NanaRouter error handler
postHandler?, // runs after request completion
)
router.use(middleware) // Add middleware to a routerNanaController
// handler: (context) => rawData | void
const controller = new NanaController<ContextType>(handler)
// the returned data from handler will be transformed before sending to action.
router.get(route, controller) // Use controller in a route
router.get(route, handler) // or directly pass a handler functionExamples
REST API
const server = new NanaServer()
const api = server.use('/api')
// Add request logging
api.use(new NanaMiddleware(({ req }) => {
console.log(`${req.method} ${req.path}`)
}))
// Users endpoint
const users = api.use('/users')
users.get('/', () => getAllUsers())
users.get('/:id', ({ id }) => getUser(id))
users.post('/', ({ body }) => createUser(body))Authentication Flow
const authMiddleware = new NanaMiddleware<{ user: User }>(
async ({ req }) => {
const user = await authenticateRequest(req)
if (!user) throw new NanaError(401, 'Unauthorized')
return { user }
}
)
const apiRouter = server.use('/api')
apiRouter.use(authMiddleware)
apiRouter.get('/dashboard', ({ user }) => ({ welcome: `Hello ${user.name}!` }))