npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@harlos/nana

v0.1.1

Published

A minimal, type-safe Node.js backend framework

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/nana

Core 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 router

NanaController

// 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 function

Examples

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}!` }))

License

MIT