AdaptiveStone Framework

website·Indexed 31 days ago

https://framework.adaptivestone.com/

A development framework for building APIs quickly, featuring Express.js controllers, Mongoose model lifecycles, and an OpenAPI 3.1 document generator. It includes built-in subsystems for logging via Winston, caching, internationalization, and a CLI for generating TypeScript types. The framework provides modular extensions for email handling and image resizing via @adaptivestone/framework-module-resize.

Tokens
39.4K
Snippets
198
Records
246
Agent score
50%

What's inside AdaptiveStone

  1. Overview of Internationalization (i18n) in the framework

    The framework provides built-in support for multi-language applications using the i18next library. It distinguishes between 'Your copy' (custom messages from controllers, validation schemas, and emails) and 'Framework messages' (built-in middleware and auth controller responses). Framework messages include English defaults in the code, meaning the app remains readable even without a translation setup. i18next is an optional peer dependency; it only needs to be installed if you intend to provide translations.
  2. Overview of AdaptiveStone Framework capabilities

    The AdaptiveStone framework is designed for developers of all skill levels to build APIs quickly. It leverages widely adopted technologies including Node.js, Express, Mongoose, and Winston. For scalability, the framework provides out-of-the-box support for the Node.js cluster module, Sentry for logging/error tracking, and Redis for caching.
  3. AdaptiveStone framework core features

    The AdaptiveStone framework is an ESM-only framework designed for SAAS, featuring built-in support for multiple databases and scaling. Key features include:

    • Automatically initialized controllers (no configuration required)
    • Mongoose models and a cache system
    • Winston logger with integrated Sentry support
    • Node cluster enabled by default (can be disabled in dev mode)
    • Docker-based development and production environments
    • Integrated Biome for code styling (alternative to ESLint/Prettier)
    • Out-of-the-box multi-language support
    • Full TypeScript support (allows mixing TS and JS files)
    • Ability to overwrite any built-in controller, model, or configuration
  4. Test HTTP endpoints

    The framework starts test servers on random ports. Use getTestServerURL(path) to retrieve the correct URL. Do not catch and discard fetch() errors, as rejected requests should fail the test to preserve the original error.
    import assert from 'node:assert/strict';
    import { it } from 'node:test';
    import {
      defaultAuthToken,
      getTestServerURL,
    } from '@adaptivestone/framework/tests/testHelpers.js';
    
    it('rejects an invalid request', async () => {
      const response = await fetch(getTestServerURL('/some/endpoint'), {
        method: 'POST',
        headers: {
          authorization: defaultAuthToken,
          'content-type': 'application/json',
        },
        body: JSON.stringify({ invalid: true }),
      });
    
      assert.equal(response.status, 400);
    });
  5. Migrate from YupFile to File type

    The YupFile helper in @adaptivestone/framework/helpers/yup.js is deprecated and will be removed in v6. Replace it with the File export from @adaptivestone/framework/types.js, which is compatible with any validator.
  6. Use the Framework cluster runner for standalone hosts

    For standalone Node deployments on a single multi-core host without an external supervisor, use the runCluster() helper. The primary process forks workers and restarts them upon abnormal exit. On SIGTERM or SIGINT, the primary drains connections and terminates workers after the shutdownTimeoutMs period.

    • workers: 'auto': Uses Node's available parallelism.
    • workers: <number>: Pins the worker count.
    • onEvent: Optional callback for structured observability events.
    // src/index.ts
    import { runCluster } from '@adaptivestone/framework/cluster.js';
    
    await runCluster(
      async () => {
        // Dynamic import ensures the primary never constructs a Server
        await import('./server.ts');
      },
      {
        workers: 'auto',
        shutdownTimeoutMs: 30_000,
      },
    );
    NODE_ENV=production node src/index.ts
  7. Create custom middleware that provides typed data to handlers

    Extend AbstractMiddleware to add data to req.appInfo. To ensure downstream handlers have the correct TypeScript types for this data, implement a static get provides() method. The return value of provides should be an empty object cast to the desired type; the framework's codegen uses this cast to type the generated request aliases.
    // src/middleware/WithArticleCount.ts
    import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";
    import type { FrameworkRequest } from "@adaptivestone/framework/services/http/HttpServer.js";
    import type { Response, NextFunction } from "express";
    
    class WithArticleCount extends AbstractMiddleware {
      static get description() {
        return "Adds the total article count to req.appInfo";
      }
    
      static get provides() {
        // Only the cast type matters for codegen; runtime ignores the value
        return {} as { articleCount: number };
      }
    
      async middleware(
        req: FrameworkRequest & { appInfo: { articleCount?: number } },
        res: Response,
        next: NextFunction,
      ) {
        const Article = this.app.getModel("Article");
        req.appInfo.articleCount = await Article.countDocuments();
        return next();
      }
    }
    
    export default WithArticleCount;
  8. Manually infer request types without codegen

    If codegen is not used, you can manually extract the output type from a Standard Schema validator using StandardSchemaV1.InferOutput. However, middleware-contributed fields in req.appInfo must be manually intersected in the handler signature.
    import type { StandardSchemaV1 } from "@adaptivestone/framework/services/validate/types";
    import { object, string } from "yup";
    
    const loginSchema = object({
      email: string().email().required(),
      password: string().required(),
    });
    
    type LoginRequest = StandardSchemaV1.InferOutput<typeof loginSchema>;
    
    async postLogin(
      req: FrameworkRequest & { appInfo: { request: LoginRequest } },
      res: Response,
    ) {
      // req.appInfo.request.email is now typed
    }
  9. Define typed fields in middleware using the provides getter

    To allow downstream handlers to access typed fields in req.appInfo, a middleware class must implement a static get provides() getter. This getter returns an empty object cast to the desired type. The framework's codegen uses this cast to provide type safety for handlers in the route's middleware chain, while the runtime ignores the return value.
    class GetUserByToken extends AbstractMiddleware {
      static get provides() {
        return {} as { user?: InstanceType<TUser> };
      }
    
      async middleware(req, res, next) {
        // ... runtime logic to populate req.appInfo.user ...
        next();
      }
    }
  10. Manage test users and authentication

    The framework does not create users automatically. Use createDefaultTestUser() to create a default User and token. For projects with custom User models, implement a custom creation helper and use setDefaultUser() and setDefaultAuthToken().
    import {
      createDefaultTestUser,
      defaultAuthToken,
      defaultUser,
    } from '@adaptivestone/framework/tests/testHelpers.js';
    
    const { user, token } = await createDefaultTestUser();
  11. Create and start a new AdaptiveStone project

    The recommended way to start a project is by cloning the official template and running it via Docker.
    # Clone the template project
    git clone git@github.com:adaptivestone/framework-example-project.git adaptivestone-example-rename-me
    
    # Navigate to the project directory
    cd adaptivestone-example-rename-me
    
    # Start the development server using Docker Compose
    docker compose up

    Once started, the site is accessible at http://localhost:3300. Changes to controllers (e.g., src/controllers/Person.ts) trigger an automatic reload.

  12. Pre-warm image generation at upload

    Call prewarm() after saving a media document to trigger background generation before the first user requests the image. This uses the same transport and worker setup as lazy mode. The method returns { enqueued: number }, representing the number of variants successfully handed to the transport after lock filtering. It does not return created or failed statuses, as processing happens asynchronously in the worker.
    import { getResizer } from '@adaptivestone/framework-module-resize';
    import { previewFormats, thumbnailSizes } from './mediaSizes.ts';
    
    const { enqueued } = await getResizer().prewarm({
      media: fileDoc,
      sizes: thumbnailSizes,
      formats: previewFormats,
    });