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

@sbfw/fastify-microservice

v1.0.0

Published

A Fastify-based microservice framework with pluggable auth, permissions, and file storage

Readme

@sbfw/fastify-microservice

A Fastify-based microservice framework with pluggable auth, permissions, and file storage.

Features

  • Pluggable auth — implement SessionResolver<T> to wire any token/session backend
  • Post-handler permission guardPostHandlerGuard verifies permission checks ran before the response is sent
  • File storage adapters — built-in Azure Blob and AWS S3, extensible via FileStorageAdapter
  • Injectable logger — provide your Winston/Pino instance or fall back to console
  • Configurable API prefix — default /api/v1, change for versioned APIs
  • OpenAPI/Swagger — enabled via OPEN_API_DOCS env var
  • CORS, multipart, file serve — opt-in via server options

Installation

npm install @sbfw/fastify-microservice fastify @fastify/cors @fastify/multipart @fastify/swagger @fastify/swagger-ui @sbfw/core

For Azure Blob storage:

npm install @azure/storage-blob

For AWS S3 storage:

npm install @aws-sdk/client-s3

Quick Start

import { FastifyServerBase, FastifyEndpoints, RequestController } from '@sbfw/fastify-microservice';
import { MySessionResolver } from './my-session-resolver.js';

class HealthController extends RequestController {}

const healthEndpoints = new FastifyEndpoints(HealthController, new MySessionResolver());

healthEndpoints.get('/health', {
  auth: false,
  outputSchema: { type: 'object', properties: { status: { type: 'string' } } },
  handler: async () => ({ status: 'ok' }),
});

const server = FastifyServerBase.init({
  serviceName: 'my-service',
  port: 3000,
  models: [],
  endpoints: [{ setup: healthEndpoints, options: { prefix: 'health' } }],
});

await server.start();

Core Concepts

SessionResolver

Implement SessionResolver<TSession> to plug in your auth system. Called on every protected route's onRequest hook.

import { SessionResolver, BaseSessionData, AuthorizationRequirements } from '@sbfw/fastify-microservice';
import { FastifyRequest, FastifyReply } from 'fastify';

interface MySession extends BaseSessionData {
  userId: string;
  role: string;
  companyId: string;
}

class MySessionResolver implements SessionResolver<MySession> {
  async authorize(
    request: FastifyRequest,
    reply: FastifyReply,
    requirements: AuthorizationRequirements,
    allowNoSession = false,
  ): Promise<MySession | null> {
    const token = request.headers['x-session-token'] as string;
    if (!token) {
      if (!allowNoSession) reply.status(401).send('token-missing');
      return null;
    }
    const session = await db.sessions.findOne({ token });
    if (!session) {
      if (!allowNoSession) reply.status(401).send('no-session');
      return null;
    }
    if (requirements.minRole && session.role !== requirements.minRole) {
      reply.status(403).send('insufficient-role');
      return null;
    }
    return session;
  }
}

FastifyEndpoints

Extend or instantiate FastifyEndpoints to define typed routes:

import { FastifyEndpoints, RequestController } from '@sbfw/fastify-microservice';

class ArticleController extends RequestController<MySession> {
  async getArticle(id: string) {
    return db.articles.findOne(id);
  }
}

const articleEndpoints = new FastifyEndpoints(ArticleController, new MySessionResolver());

// Public endpoint
articleEndpoints.get('/articles/:id', {
  auth: false,
  params: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
  outputSchema: { type: 'object', properties: { title: { type: 'string' } } },
  handler: async ({ params }) => db.articles.findOne(params!.id),
});

// Protected endpoint
articleEndpoints.post('/articles', {
  auth: { minRole: 'editor' },
  inputSchema: { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] },
  outputSchema: { type: 'object', properties: { id: { type: 'string' } } },
  handler: async ({ body, session }) => db.articles.create({ ...body, author: session!.userId }),
});

Available methods: get, post, put, patch, delete, deleteInputSchema.

afterAuth hook

Override afterAuth in a subclass to run logic after auth on every protected request (e.g., inject language headers):

class MyEndpoints extends FastifyEndpoints<MyController> {
  protected async afterAuth(request, reply): Promise<void> {
    request.headers['accept-language'] ??= request.session?.preferredLanguage ?? 'en';
  }
}

PostHandlerGuard

Implement PostHandlerGuard to verify that fine-grained permission checks ran inside the handler before the response is sent. The framework registers an onSend hook that returns 403 if the guard fails.

import { PostHandlerGuard, PostHandlerGuardResult } from '@sbfw/fastify-microservice';
import { FastifyRequest } from 'fastify';

const myGuard: PostHandlerGuard = {
  shouldRun(request: FastifyRequest): boolean {
    return !!(request as any).permissionChecker;
  },
  check(request: FastifyRequest): PostHandlerGuardResult {
    const checker = (request as any).permissionChecker;
    return {
      permitted: checker.isPermitted(),
      fullyExecuted: checker.isComplete(),
      message: 'Access restricted.',
    };
  },
};

FastifyServerBase.init({
  serviceName: 'my-service',
  postHandlerGuard: myGuard,
  // ...
});

FileStorageAdapter

Implement FileStorageAdapter or use the built-in Azure/S3 adapters:

import { AzureStorageAdapter } from '@sbfw/fastify-microservice/storage';
// or
import { S3StorageAdapter } from '@sbfw/fastify-microservice/storage';

const storage = new AzureStorageAdapter();
await storage.initialize();

FastifyServerBase.init({
  serviceName: 'my-service',
  storageAdapter: storage,
  enableFileUpload: true,
  enableFileServe: true,
  // ...
});

Custom adapter

import { FileStorageAdapter, IUploadedFile, IUploadOptions } from '@sbfw/fastify-microservice';

class InMemoryStorageAdapter implements FileStorageAdapter {
  private store = new Map<string, { buffer: Buffer; contentType: string }>();

  async upload(buffer, filename, mimeType, options): Promise<IUploadedFile> {
    const key = `${options.storagePath}/${filename}`;
    this.store.set(key, { buffer, contentType: mimeType });
    return { url: key, storagePath: options.storagePath, filename, mimeType, size: buffer.length, blobName: key };
  }

  async download(blobName) {
    const entry = this.store.get(blobName)!;
    return { buffer: entry.buffer, contentType: entry.contentType };
  }

  async delete(blobName) {
    return this.store.delete(blobName);
  }

  async exists(blobName) {
    return this.store.has(blobName);
  }
}

Logger

Provide your own logger via IFrameworkLogger. If not provided, console is used.

import winston from 'winston';
import { IFrameworkLogger } from '@sbfw/fastify-microservice';

const winstonLogger = winston.createLogger({ /* ... */ });

const frameworkLogger: IFrameworkLogger = {
  log(level: string, message: string) {
    winstonLogger.log(level, message);
  },
};

FastifyServerBase.init({
  serviceName: 'my-service',
  frameworkLogger,
  // ...
});

API Prefix

The default route prefix is /api/v1. Override it for versioned APIs:

FastifyServerBase.init({
  serviceName: 'my-service',
  apiPrefix: '/api/v2',
  // ...
});

Use apiPrefix: '/' for bare setups without a prefix.

Server Options

| Option | Type | Default | Description | |---|---|---|---| | serviceName | string | required | Used for logging and Swagger title | | port | number | 3000 / PORT env | Port to listen on | | host | string | 0.0.0.0 / HOST env | Host to bind | | models | ModelSetupHandler[] | required | Fastify schema registration callbacks | | endpoints | EndpointSetup[] | required | Endpoint registration entries | | apiPrefix | string | '/api/v1' | Route prefix for all endpoints | | cors | string \| string[] \| null | CORS env | CORS origin(s) | | frameworkLogger | IFrameworkLogger | console | Injectable logger | | postHandlerGuard | PostHandlerGuard | none | Post-response permission guard | | storageAdapter | FileStorageAdapter | none | Required when enableFileServe: true | | enableFileUpload | boolean | false | Register @fastify/multipart | | enableFileServe | boolean | false | Register file-serve endpoint | | swagger | SwaggerInfoOptions | {} | OpenAPI info overrides | | onStart | () => Promise<void> | none | Called before server starts listening | | onStop | () => Promise<void> | none | Called on graceful shutdown |

Environment Variables

| Variable | Description | |---|---| | PORT | Server port (default: 3000) | | HOST | Server host (default: 0.0.0.0) | | CORS | Comma-separated CORS origins | | OPEN_API_DOCS | Enable Swagger UI (1, true, or a URL fragment filter) | | VIRTUAL_PATH | Virtual path prefix for docker-compose Swagger server entry | | Azure adapter | | | AZURE_STORAGE_CONNECTION_STRING | Azure Storage connection string | | AZURE_STORAGE_CONTAINER_NAME | Default blob container name | | S3 adapter | | | AWS_REGION | AWS region (default: us-east-1) | | AWS_S3_BUCKET | Default S3 bucket name | | AWS_ACCESS_KEY_ID | AWS access key (optional, falls back to SDK credential chain) | | AWS_SECRET_ACCESS_KEY | AWS secret key (optional) |

Error Classes

| Class | Status | Description | |---|---|---| | InputApiError | 400 | Validation / bad input | | UnauthorizedApiError | 401 | Missing or invalid credentials | | ForbiddenApiError | 403 | Insufficient permissions | | NotFoundApiError | 404 | Resource not found | | BusinessLogicApiError | 409 | Business rule violation | | MicroserviceApiError | 500 | Downstream service failure | | StorageApiError | 500 | File storage failure | | ServerError | 500 | Unexpected server error |

All extend BaseApiError which implements Fastify's FastifyError interface.

Schema Utilities

import { objectSchema, arraySchema, extendObjectSchema, errorResponse } from '@sbfw/fastify-microservice';

const UserSchema = objectSchema<User>(
  { id: { type: 'string' }, name: { type: 'string' }, email: { type: 'string' } },
  { includeRequired: ['id', 'name'] },
);

const UsersListSchema = arraySchema<User>(
  { id: { type: 'string' }, name: { type: 'string' } },
);

const AdminUserSchema = extendObjectSchema<User, { adminFlag: boolean }>({
  base: { id: { type: 'string' }, name: { type: 'string' } },
  extension: { adminFlag: { type: 'boolean' } },
  exclude: ['name'],
});