Versioning Nestjs routes? - nestjs

I'm just getting started with Nestjs and am wondering how I can version my API using either a route prefix or through an Express Router instance?
Ideally, I would like to make endpoints accessible via:
/v1
/v2
etc, so I can gracefully degrade endpoints. I'm not seeing where I could add the version prefix. I know it's possible to set a global prefix on the application instance, but that's not for a particular set of endpoints.

Here's an open discussion about the RouterModule https://github.com/nestjs/nest/issues/255. I understand how important this functionality is, so it should appear in the near future. At this point it is necessary to put v1 / v2 directly into the #Controller() decorator.

Router Module comes to rescue, with Nest RouterModule it's now a painless organizing your routes.
See How it easy to setup.
const routes: Routes = [
{
path: '/ninja',
module: NinjaModule,
children: [
{
path: '/cats',
module: CatsModule,
},
{
path: '/dogs',
module: DogsModule,
},
],
},
];
#Module({
imports: [
RouterModule.forRoutes(routes), // setup the routes
CatsModule,
DogsModule,
NinjaModule
], // as usual, nothing new
})
export class ApplicationModule {}
this will produce something like this:
ninja
├── /
├── /katana
├── cats
│ ├── /
│ └── /ketty
├── dogs
├── /
└── /puppy
and sure, for Versioning the routes you could do similar to this
const routes: Routes = [
{
path: '/v1',
children: [CatsModule, DogsModule],
},
{
path: '/v2',
children: [CatsModule2, DogsModule2],
},
];
Nice !
check it out Nest Router

for version or any prefix, you can use "global prefix":
https://docs.nestjs.com/faq/global-prefix

according to latest docs inside main.ts after const app = await NestFactory.create(AppModule) use
// Versioning
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
prefix: 'api/v',
});
This will give all your routes a default prefix of /api/v1 unless specified
To override version in controller use
#Controller({version:'2'}) decorator on controller class
For overriding version at route level use #Version('2') above controller method
Note: If you are using swagger make sure to call app.enableVersioning() before SwaggerModule.createDocument()
Link: https://docs.nestjs.com/techniques/versioning

Best and simple way to do this is using global prefix
An example is given below :
import { VersioningType } from "#nestjs/common";
app.enableVersioning({
type: VersioningType.URI,
});
app.setGlobalPrefix("api/v1"); //edit your prefix as per your requirements!
You can exclude routes from the global prefix using the following construction:
app.setGlobalPrefix('v1', {
exclude: [{ path: 'health', method: RequestMethod.GET }], // replace your endpoints in the place of health!
});
Alternatively, you can specify route as a string (it will apply to every request method):
app.setGlobalPrefix('v1', { exclude: ['cats'] }); // replace your endpoints in the place of cats!

Related

Vite library with Windicss

I am using Vite (Vue3) with Windi CSS to develop a library. I am using library mode for the build (https://vitejs.dev/guide/build.html#library-mode) with the following config:
vite.config.js
export default defineConfig({
plugins: [vue(), WindiCSS()],
build: {
lib: {
entry: path.resolve(__dirname, 'src/lib.js'),
name: 'MyLIB',
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['vue'],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: 'Vue',
},
},
},
},
});
My entry file (src/lib.js) only includes a few Vue components in it and looks like this:
lib.js
export { default as AButton } from './components/AButton/AButton.vue';
export { default as ACheckbox } from './components/ACheckbox/ACheckbox.vue';
import 'virtual:windi.css';
import './assets/fonts.css';
When I build the library I get the js for just those components but the css is for every Vue file in the src folder and not only the ones i included in my lib.js file. I know the default behavior for Windi CSS is to scan the whole src folder but in this case, I only want it to scan the components I added to my entry.
Any ideas?
You should be able to restrict the scan by using extract.include and extract.exclude options, see there : https://windicss.org/guide/extractions.html#scanning
From the doc
If you want to enable/disable scanning for other file-types or locations, you can configure it using include and exclude options

Package.json exports with webpack 5 - dynamically imported module not found

I am having a bit of trouble reconciling the path of a dynamic import for i18n locales. Here's the relevant code -
function getLoader(
lang: SupportedLanguage,
ns: SupportedNamespace
): NamespaceLoader | undefined {
const matrixToCheck = UNSUPPORTED_MATRIX[ns];
const isSupported = matrixToCheck && matrixToCheck.indexOf(lang) === -1;
if (isSupported) {
const path = `./locales/${lang}/${ns}.json`;
const name = `${lang}_${ns}`;
const named = {
[name]: () => import(`${path}`),
};
return named[name];
}
}
...
// eventual output
const SUPPORTED_LANGUAGES = {en: {namespace1: () => import('./locales/en/namespace1.json')}
My goal is manage all of the relevant translations in a single npm package, handle all of the dynamic import set-up at build time, and then consumers can invoke the getter (getTranslation in this case) in their respectives apps for the language and namespace of their choice to get the payload at runtime.
Based on this GH thread, I wanted to reconcile the locale dist path via the package.json
...
"exports": {
".": "./dist/src/main.js",
"./": "./dist/"
},
...
e.g. when I publish the package, based on that exports config, the consumer would know know how to reconcile the path, either relative or package-name-prefix when the getter is invoked
const fn = () => import('./locales/fr/myNamespace.json') /// doesn't work
const anotherFn = () => import('#examplePackageName/locales/fr/myNamespace.json') /// doesn't work
Since everything is dynamic, I am using the CopyWebpackPlugin to include the locales in the dist folder.
This works as expected locally, but when I create the dist, I get the error Error: Module not found ./relative/path/to/the/json/I/want.json.
My question:
What am I missing? Is there a simple way to expose these translations so that other apps can include them in their bundles via an npm-installed package?
Here's my Webpack config, happy to provide other info as needed
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const getPlugins = () => {
return [
new CleanWebpackPlugin(),
new CopyPlugin({
patterns: [{ from: "locales", to: "locales" }],
}),
];
};
module.exports = {
mode: "production",
entry: {
main: "./src/main.ts",
},
output: {
path: path.join(__dirname, "dist"),
filename: "src/[name].js",
chunkFilename: "chunk.[name].js",
libraryTarget: "commonjs2",
},
resolve: {
extensions: [".json", ".ts", ".js"],
alias: {
"#locales": path.resolve(__dirname, "locales/*"),
},
},
plugins: getPlugins(),
module: {
rules: [
{
test: /\.ts$/,
exclude: [/\.test\.ts$/],
include: path.join(__dirname, "src"),
loader: "ts-loader",
},
],
},
};
Exports directive prescribes to define all files allowed for import explicitly (documentation). It allows developer to hide internal package file structure. What's not exported by this directive is only available to import inside the package and not outside of it. It's made to simplify maintenance. It allows developers to rename files or change file structure without fear of breaking dependent packages and applications.
So if you want to make internal files visible for import, you should export them with exports directive explicitly, like this:
{
"exports": {
".": "./dist/esm/src/main.js",
"./dist/shared/locale/fr_fr.json": "./dist/shared/locale/fr_fr.json"
}
}
I'm not sure wether Webpack handling this case, because it's an experimental feature yet. But this is how Node.js works now.
Why it is so
Changing your app file structure is a major change in semver terms, so you need to bump a version everytime you rename or delete files. To avoid it you can specify which files are part of public interface of the package.

Node.js: How to import test files in custom test runner

I'm trying to create my own custom testing framework for learning purpose. Test files are written in following way
import { somemethod } from './some/module'
test(/I click on a button)/, () => {
browser.get("someSelector").should("have.text",somemethod());
});
I user require(file) to load test files. But it throw error SyntaxError: Unexpected token {
for import statement in test file. I'm using node js version 11.15.
If I switch to node v13.14 and define "type": "module" in my package.json then it doesn't let me use require(file) to load a test file or any module in my package.
How can I import tests files considering the user may be importing the modules using import or require?
This answer is very empirical...
Considering that it works using canonical commonjs approach you can try to debug it with newer version of NODE (currently I would use 14). For it, I would suggest you to use a node version manager like NVM so you can switch between node version easily and test that accordling seeing differences between various node installations.
Make a minimal project with npm init with a single dependency, save your index with the .mjs extension and try an import the above dependency. If you are be able to import that dependency with that minimal environment you can blame either your previous node or your configuration or both of them.
At the moment you should only create a small 2 files project to reproduce the problem. It seems your current node does not consider the "type": "module" configuration and runs everything in its classic way.
Regarding your comments....
As far as I know import can be used even in your code, not just at the beginning:
(async () => {
if (somethingIsTrue) {
// import module for side effects
await import('/modules/my-module.js');
}
})();
from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
Additionally you can try Webpack with a configuration like:
// webpack.config.js
const nodeExternals = require('webpack-node-externals');
module.exports = {
mode: 'production',
target: 'node',
externals: [nodeExternals()],
entry: {
'build/output': './src/index.js'
},
output: {
path: __dirname,
filename: '[name].bundle.js',
libraryTarget: 'commonjs2'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['env', {
'targets': {
'node': 'current'
}
}]
]
}
}
}]
}
};
With NodeExternals you don't put your node dependencies in the bundle but only your own code. You refer to node_modules for the rest. You might not want that.

Unit test for aws lambda using jest

const invokeApi = require("/opt/nodejs/kiwiCall");
const decrypt = require("/opt/nodejs/encryption");
const cors = require("/opt/nodejs/cors");
When I am testing my index.js file by manual mocking these dependencies in mocks directory as follows:
__mocks__
|_invokeApi
|_decrypt
|_cors
it says
FAIL ./index.test.js
● Test suite failed to run
Cannot find module '/opt/nodejs/kiwiCall' from 'index.js'
However, Jest was able to find:
'../../../../lambdas/Flights/Locations/index.js'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['js', 'json', 'jsx', 'ts', 'tsx', 'node'].
See https://jestjs.io/docs/en/configuration#modulefileextensions-array-string
1 | "use strict";
2 |
> 3 | const invokeApi = require("/opt/nodejs/kiwiCall");
Wanted to know how can I mock the dependencies of AWS lambda in inedx.test.js file
In your package.json or jest.config you could add a moduleNameMapper for that directory.
"jest": {
"moduleNameMapper": {
"/opt/nodejs/(.*)": "<rootDir>/../nodejs/$1"
},
},
So I managed to figure out something based on my repository.
I'm using the moduleNameMapper to map the absolute path to another location in my repository to where I have the layer stored.
Eg.
moduleNameMapper: {'^/opt/config/config': '<rootDir>/src/layers/layers-core/config/config'}
In your case you could use a regex expression to match /opt/nodejs/ and map it elsewhere. Hope that helped.
EDIT:
I completely changed my approach and used babel-plugin-module-resolver with babel-rewire. I did this because the above method was incompatible with rewire. It's quite easy setup and you just need to setup a babel alias within .babelrc.
eg.
{
"plugins": [
["rewire"],
["babel-plugin-module-resolver", {
"alias": {
"/opt/config/config": "./src/layers/layers-core/config/config",
"/opt/utils/util-logger": "./src/layers/layers-core/utils/util-logger",
"/opt/slack": "./src/layers/layers-slack/slack"
}
}]
]
}
Combine this with IDE jsconfig.json path alias and you get full IDE support.
{
"compilerOptions": {
"module": "commonjs",
"target": "es2018",
"baseUrl": "./",
"paths": {
"/opt/config/config": ["src/layers/layers-core/config/config"],
"/opt/utils/util-logger": ["src/layers/layers-core/utils/util-logger"],
"/opt/slack/*": ["src/layers/layers-slack/slack/*"],
}
},
"exclude": ["node_modules", "dist"]
}
You can then reference your layers with jest.doMock('/opt/config/config', mockConfig);
EDIT 2:
Found a way to get Jest to mock it. Just slip {virtual: true} into the mock!
jest.doMock('/opt/config/config', mockConfig, {virtual: true});
I have pretty much the same issue. I have defined a layer which contains common code that's shared between other functions in my project. My project structure looks something like this:
project/
functions/
function1/
app.js
function2/
app.js
shared/
shared.js
I import my shared library like this:
const { doSomething } = require('/opt/shared');
exports.handler = async (event) => {
const result = await doSomething();
// etc...
return {statusCode: 200};
}
This works when I deploy to AWS Lambda because the /opt/shared exists and it can be referenced correctly. It also works if I run this on my machine using sam local invoke Function1 because it's running in a container, which makes /opt/shared available to the code.
However, I'm struggling to work out how I can mock this dependency in a unit test. If I simply do this: jest.mock('/opt/shared'), I'm getting: Cannot find module '/opt/shared' from app.test.js
You can use the modulePaths option, from this post.
Documentation
jest.config.js
"jest": {
"modulePaths": [
"<rootDir>/src/layers/base/nodejs/node_modules/"
]
}
You can dynamically create this array by scanning a directory
const testFolder = './functions/';
const fs = require('fs');
const modulePaths = fs.readdirSync(testFolder)
.reduce((modulePaths, dirName) => {
modulePaths.push(`functions/${dirName}/dependencies/nodejs/node_modules/`);
return modulePaths;
}, []);

Including a shared project in create-react-app

In my project i have the following folder/project structure.
Project
├── client (create-react-app)
│ ├── tsconfig.json
│ └── packages.json
├── server
│ ├── tsconfig.json
│ └── packages.json
└── shared
├── tsconfig.json
└── packages.json
The shared project is mostly interface/class type declarations that i use in both the front and backend so they use the same object type.
I was including the shared project in both client and server using the referenced projects from typescript.
tsconfig.json
...
},
"references": [
{
"path": "../shared"
}
],
Although upon trying to use one of the imported types in a useReducer hook:
Component in client/src
import React, { useReducer, useEffect } from 'react';
import { IRoadBook, GetNewRoadBook} from '../../../shared/src/types/IRoadBook';
import { Grid, TextField, Button } from '#material-ui/core';
import { useParams } from 'react-router-dom';
type State = {
RoadBook: IRoadBook | any;
}
export default function RoadBookEditor() {
const { objectId } = useParams();
const [state, dispatch] = useReducer(reducer, {RoadBook : GetNewRoadBook()});
....
The following error happens upon starting the project :
"Module not found: You attempted to import ../../../shared/src/types/IRoadBook which falls outside of the project src/ directory. Relative imports outside
of src/ are not supported."
Which from what i read is a limitation imposed by create-react-app configurations.
If i do
const [state, dispatch] = useReducer(reducer, {RoadBook :{}});
Code builds/runs successfuly. (causes visual issues down the road but that is not the point of this question).
Question : What is the proper way to include this shared project in my create-react-app client project?
shared/types/IRoadBook.tsx
export interface IRoadBook {
_id: string;
Name: string;
Description: string;
}
export function GetNewRoadBook(): IRoadBook {
return { _id: '', Name: '', Description: '' } as IRoadBook;
}
If I understood correctly what you're looking for is a monorepo setup.
Out of the box create-react-app does not support importing code from outside your source folder. To achieve it you need to modify the webpack config inside it, and there are a couple of alternatives for that:
1- Using something like customize-cra, craco, etc.
2- forking CRA and modifying react-scripts (https://create-react-app.dev/docs/alternatives-to-ejecting/)
3- Ejecting and modifying the webpackconfig
for 2 and 3, you'll need to remove ModuleScopePlugin from webpack config and add the paths to babel-loader.
A few other places you can get a direction:
Create React App + Typescript In monorepo code sharing
https://blog.usejournal.com/step-by-step-guide-to-create-a-typescript-monorepo-with-yarn-workspaces-and-lerna-a8ed530ecd6d

Resources