Nestjs swagger ui not loading styles when deployed to vercel but works well locally
console and network requests
I added vercel.json with the following configuration and deployed to vercel.
{
"version": 2,
"builds": [
{
"src": "src/main.ts",
"use": "#vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "src/main.ts",
"methods": ["GET", "POST", "PUT", "PATCH", "DELETE"]
}
]
}
main.ts
const swaggerConfig = new DocumentBuilder()
.setTitle('Tansfun')
.setDescription('API for Tansfun')
.setVersion('1.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'APIKey',
name: 'APIKey',
description: 'Enter API Key',
in: 'header',
},
'APIKey-auth',
)
.build();
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const document = SwaggerModule.createDocument(app, swaggerConfig);
app.useGlobalPipes(new ValidationPipe());
SwaggerModule.setup('api', app, document);
await app.listen(port);
}
bootstrap();
I used #nestjs/swagger v6
I recently came across this issue.
Fortunately I found a working working solution
The solution is a bit hacky tho
The First Solution
Is to get you api's swagger JSON file host it and use it with a swagger ui explorer
Serve your swagger JSON file statically with nestjs
Get the path to the swagger JSON file on your vercel server
Use it with a swagger ui explorer
How To Achieve Solution 1
Steps
On your local machine / development machine set the NODE_ENV variable to development.
In your .env file
NODE_ENV="development"
Create a static folder in your projects root. eg: swagger-static
Statically serve content of the swagger-static folder
here is link to documentation on serving static files with nestjs
In your app.module.ts
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ServeStaticModule } from '#nestjs/serve-static';
import { join } from 'path';
#Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'swagger-static'),
serveRoot: process.env.NODE_ENV === 'development' ? '/' : '/swagger',
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Every time your app starts in development you then to generate your api
s swagger json and store in a swagger.json file in the swagger-static folder in your prjects root directory
This issue on github discusses and has a solution on how to generate a swagger JSON file for your api
Below is a code snippet on how to generate the swagger.json file
In your main.ts
import { NestFactory } from '#nestjs/core';
import { SwaggerModule, DocumentBuilder } from '#nestjs/swagger';
import { AppModule } from './app.module';
import { resolve } from 'path';
import { writeFileSync } from 'fs';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const options = new DocumentBuilder()
.setTitle('Cats example')
.setDescription('The cats API description')
.setVersion('1.0')
.addTag('cats')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('/swagger', app, document);
await app.listen(process.env.PORT || 3000);
// get the swagger json file (if app is running in development mode)
if (process.env.NODE_ENV === 'development') {
const pathToSwaggerStaticFolder = resolve(process.cwd(), 'swagger-static');
// write swagger json file
const pathToSwaggerJson = resolve(
pathToSwaggerStaticFolder,
'swagger.json',
);
const swaggerJson = JSON.stringify(document, null, 2);
writeFileSync(pathToSwaggerJson, swaggerJson);
console.log(`Swagger JSON file written to: '/swagger-static/swagger.json'`);
}
}
bootstrap();
Now every time your app starts in development the swagger JSON file with the generated
In production it would be served on your vercel domain eg: https://yourprojectname.vercel.app/swagger/swagger.json
Push you changes to vercel and test your swagger api by using the path to the swagger.json file on your server
Eg: Head to the swagger ui explorer page https://petstore.swagger.io/?_ga=2.160760958.2144886769.1670328433-858019792.1670328433#/. On the the page enter the path to your swagger.json file in the explorer input and click explore. Your swagger docs your now be loaded
The Second Solution (recommend)
Is to get missing swagger files in development and manually serve them statically on vercel (your production serve)
How To Achieve Solution 2
Steps
On your local machine / development machine set the NODE_ENV variable to development.
In your .env file
NODE_ENV="development"
Create a static folder in your projects root. eg: swagger-static
Statically serve content of the swagger-static folder
here is link to documentation on serving static files with nestjs
In your app.module.ts
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ServeStaticModule } from '#nestjs/serve-static';
import { join } from 'path';
#Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'swagger-static'),
serveRoot: process.env.NODE_ENV === 'development' ? '/' : '/swagger',
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Every time your app starts in development you make and http call to fetch the missing swagger ui resources on your production server.
In my case the missing files were swagger-ui-bundle.js, swagger-ui-init.js, swagger-ui-standalone-preset.js and swagger-ui.css
In your main.ts file after your app has started check if your app is in development an fetch the missing swagger resources then store them in the swagger-static folder in your root directory
import { NestFactory } from '#nestjs/core';
import { SwaggerModule, DocumentBuilder } from '#nestjs/swagger';
import { AppModule } from './app.module';
// core
import { resolve } from 'path';
import { writeFileSync, createWriteStream } from 'fs';
import { get } from 'http';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const options = new DocumentBuilder()
.setTitle('Cats example')
.setDescription('The cats API description')
.setVersion('1.0')
.addTag('cats')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('/swagger', app, document);
await app.listen(process.env.PORT || 3000);
// get the swagger json file (if app is running in development mode)
if (process.env.NODE_ENV === 'development') {
// write swagger ui files
get(
`${serverUrl}/swagger/swagger-ui-bundle.js`, function
(response) {
response.pipe(createWriteStream('swagger-static/swagger-ui-bundle.js'));
console.log(
`Swagger UI bundle file written to: '/swagger-static/swagger-ui-bundle.js'`,
);
});
get(`${serverUrl}/swagger/swagger-ui-init.js`, function (response) {
response.pipe(createWriteStream('swagger-static/swagger-ui-init.js'));
console.log(
`Swagger UI init file written to: '/swagger-static/swagger-ui-init.js'`,
);
});
get(
`${serverUrl}/swagger/swagger-ui-standalone-preset.js`,
function (response) {
response.pipe(
createWriteStream('swagger-static/swagger-ui-standalone-preset.js'),
);
console.log(
`Swagger UI standalone preset file written to: '/swagger-static/swagger-ui-standalone-preset.js'`,
);
});
get(`${serverUrl}/swagger/swagger-ui.css`, function (response) {
response.pipe(createWriteStream('swagger-static/swagger-ui.css'));
console.log(
`Swagger UI css file written to: '/swagger-static/swagger-ui.css'`,
);
});
}
}
bootstrap();
Now every time your app starts in development the missing swagger would be fetched localy and stored in the swagger-static folder
In production file missing would be served upon request on your vercel server
Push you changes to vercel and test your swagger. Everything should be working now
try with this, set the custom js and css
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// swagger setup
const config = new DocumentBuilder()
.setTitle('Backend Generator')
.setDescription('Documentation API Test')
.setVersion('1.0')
.setBasePath('api/v1')
.addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' })
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('swagger', app, document, {
customSiteTitle: 'Backend Generator',
customfavIcon: 'https://avatars.githubusercontent.com/u/6936373?s=200&v=4',
customJs: [
'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.min.js',
],
customCssUrl: [
'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.min.css',
'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.min.css',
'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css',
],
});
const cors = { ...CorsConfig };
app.enableCors(cors);
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
app.setGlobalPrefix('api/v1');
useContainer(app.select(AppModule), { fallbackOnErrors: true });
await app.listen(5000);
}
bootstrap();
Related
Right now I have React app initialized with Vite connected with Sanity.
Everything is working just fine, Sanity client is connected, React is fetching data from Sanity, I'm receiving it with no problem.
But the problem is, that if I deploy React app with Sanity connected, then I will leak my projectID and sanity_token to the fronted, which I want to avoid of course.
So I have to make some backend REST API which will be fetched by React, and then my API will fetch Sanity. I could do it with Node.js and Express without problem, but I decided that I will use NestJS and learn something instead.
But when it comes to NestJS, everything is connected a bit different.
On the front I had :
import sanityClient from '#sanity/client';
export const client = sanityClient({
projectId: import.meta.env.VITE_SANITY_PROJECT_ID,
dataset: 'production',
apiVersion: '2022-02-01',
useCdn: true,
token: import.meta.env.VITE_SANITY_TOKEN
});
And for NestJS I found something like this:
import { Injectable } from '#nestjs/common';
import sanityClient, { SanityClient } from '#sanity/client';
#Injectable()
export class SanityService {
public readonly client: SanityClient = sanityClient({
projectId: process.env.SANITY_PROJECT_ID,
dataset: 'production',
apiVersion: '2022-02-01',
useCdn: true,
token: process.env.SANITY_TOKEN
});
}
My question is that if it's a good way to connect Sanity client?
How to query Sanity client with specified GROQ query?
Till now I was using this on the frontend, but it's not gonna work in NestJS:
const query = '*[_type == "blogPost"]';
client.fetch(query)
.then((data) => {
setPosts(data);
})
.catch((err) => {
console.log(err);
})
It turned out that this is the proper way to connect with Sanity client, I had an issue with that similar to this thread
And the solution was the same as in the thread above. Add "esModuleInterop": true to tsconfig.json.
{
"compilerOptions": {
...
"esModuleInterop": true,
...
}
}
Then call for data to sanity client is working properly:
#Get()
async getAllPosts() {
// initialize sanity instance
const sanityService = new SanityService();
// your query
const query = '*[_type == "blogPost"]';
try {
const data = await sanityService.client.fetch(query);
return data;
} catch (error) {
console.log(error.msg);
}
}
I have a Next.js application that has React components in the pages directory that render as expected. The application was built for a rails backend api and is using that backend every day properly. I am trying to add a twilio video chat to the app. I created an /api directory as instructed in all documentations. When I attempt to make a call to this api for literally a mock example to test the api I get the error in the terminal Error: The default export is not a React Component in page: "/api/twilio" at Object.renderToHTML (/home/application_in_question/node_modules/next-server/dist/server/render.js:117:19)
and I also get Failed to load resource: the server responded with a status of 500 (Internal Server Error) in the browser. I was not part of the team that built this application so I am unsure of why I cannot add this Next.js api. The api is the selling point of Next and it is supposedly built into the framework.
I get this error is I just put the route in the address bar of the browser but I also get it from a file api call.
pages/twilio/index.js
const handleFetchPosts = async () => {
debugger
const tokenResponse = await fetch("/api/twilio");
debugger
const tokenData = await tokenResponse.json();
debugger
setToken(tokenData);
};
section of package.json
"next": "^8.1.0",
"next-auth": "^1.8.5",
"next-compose-plugins": "^2.2.0",
"next-redux-wrapper": "^2.0.0",
"next-routes": "^1.4.2",
"next-seo": "^1.11.2",
"node-sass": "^4.12.0",
pages/api/twilio/index.js
console.log("running api")
const handler = (req, res) => {
return res.json({ hello: 'world!' });
};
export default handler;
next.config.js
const { withPlugins, optional } = require('next-compose-plugins')
// eslint-disable-next-line import/no-extraneous-dependencies
const { PHASE_PRODUCTION_BUILD } = require('next-server/constants')
const sass = require('#zeit/next-sass')
const { requireEnvVar } = require('./lib/utils')
Custom Next.js Configuration
#see https://nextjs.org/docs#custom-configuration
const nextConfig = {
Runtime configuration
#see https://nextjs.org/docs#exposing-configuration-to-the-server--client-side
publicRuntimeConfig: {
// Will be available on both server and client
apiUrl: requireEnvVar('API_SERVER'),
googleApiKey: requireEnvVar('GOOGLE_API_KEY'),
stripeApiKey: requireEnvVar('STRIPE_PUBLISHABLE_KEY'),
instantPayFee: requireEnvVar('INSTANT_PAY_FEE'),
},
Disable file-system routing
#see https://nextjs.org/docs#disabling-file-system-routing
**useFileSystemPublicRoutes: true,**
Custom webpack config
#see https://nextjs.org/docs#customizing-webpack-config
webpack(config, { webpack }) {
Only load specific locales for moment.js
#see https://stackoverflow.com/a/25426019/956688
config.plugins.push(
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /en/)
)
return config
},
}
Load multiple plugins with next-compose-plugins
#see https://github.com/cyrilwanner/next-compose-plugins
module.exports = withPlugins(
[
[sass],
Analyzing the webpack bundle
#see https://github.com/zeit/next-plugins/tree/master/packages/next-bundle-analyzer
Load #zeit/next-bundle-analyzer as an optional plugin only during production build
#see https://github.com/cyrilwanner/next-compose-plugins#optional-plugins
#see https://github.com/cyrilwanner/next-compose-plugins#phases-array
[
// eslint-disable-next-line global-require,import/no-extraneous-dependencies
optional(() => require('#zeit/next-bundle-analyzer')),
{
analyzeServer: ['server', 'both'].includes(process.env.BUNDLE_ANALYZE),
analyzeBrowser: ['browser', 'both'].includes(
process.env.BUNDLE_ANALYZE
),
**bundleAnalyzerConfig: {
server: {
analyzerMode: 'static',
reportFilename: '../../bundles/server.html',
},
browser: {
analyzerMode: 'static',
reportFilename: '../bundles/client.html',
},
}**,
},
[PHASE_PRODUCTION_BUILD],
],
],
nextConfig
)`
As you can see above the team before me had useFileSystemPublicRoutes set to false. I have made this true.
When I attempt to fetch the api in the react page or use a get request in the browser for the api/index.js file I created with the code
`
console.log("running api")
const handler = (req, res) => {
return res.json({ hello: 'world!' });
};
export default handler;`
This gives the error above. This is a very simple example that I have seen work in numerous resources online so I do not understand why this is happening.
How can I get this api to work???
** I added the ** in an attempt to highlight the parts I think could help. I already set useFileSystemPublicRoutes to true.
You can read in the comment thread Next#8 was before API routes were added to the framework. I ended up using a workaround where I used the rails backend server. Next version 9+ has api routes where any file in a pages/api directory is treated as a server backend api file. Next 8 was treating pages/api/file as a front-end webpage file looking for react.
Problem Statement:
I have working on project in Angular using AWS Amplify. The project uses cognito and successfully created AWS api for client calls. Now, I need an interceptor to intercept the requests as I need to do perform some action.
What I DID:
I have tried using Angular HTTP Interceptors but they don't work. According to my research that AWS Amplify uses axios interceptors under the hood to intercept the calls. I tried implementing using Axios package
https://www.npmjs.com/package/axios
My Implementation:
MyIntercept.ts
import { Injectable } from '#angular/core';
import axios from 'axios';
#Injectable({providedIn: 'root'})
export class MyInterceptor {
intercept() {
console.log("Hello this is my interceptor")
axios.interceptors.request.use(request => {
console.log("*******************Inside My Interceptor*************");
console.log("Call: ", request);
return request;
});
}
}
export function InterceptorFactory(myIntercept: MyInterceptor): any {
return () => myIntercept.intercept();
}
and in the app Module file
import { InterceptorFactory, MyInterceptor } from './myInterceptor.service';
import { APP_INITIALIZER } from '#angular/core';
providers: [
{
provide: APP_INITIALIZER,
useFactory: InterceptorFactory,
deps: [MyInterceptor],
multi: true
}
]
What I get:
When I run the code, all I see the line outside the console for once only which is:
Hello this is my interceptor
I need to ask you if I am doing something wrong here or is there any other approach to achieve this?
I created a Express/Node service which uses ApolloClient v3 to subscribe to a database on Hasura (database was created with Heroku on the Hasura platform). I want to subscribe to database changes with ApolloClient.
My ApolloClient setup looks like this (inspired by Split Communication Docs):
import express, { Request, Response } from "express";
import "cross-fetch/polyfill";
import ws from "ws";
import {
ApolloClient,
HttpLink,
InMemoryCache,
split,
gql,
} from "#apollo/client/core";
import { WebSocketLink } from "#apollo/client/link/ws";
import { getMainDefinition } from "#apollo/client/utilities";
const app = express();
const port = 8080;
const httpLink = new HttpLink({
uri: "https://my.hasura.app/v1/graphql",
fetch,
});
const wsLink = new WebSocketLink({
uri: "ws://my.hasura.app/v1/graphql",
options: {
reconnect: true,
},
webSocketImpl: ws,
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
Things I already discovered for using ApolloClient with Node.js:
Use the "#apollo/client/core" import and not "#apollo/client", because this needs a react dependency (and i don't want that on my backend service)
import ws from "ws"; for a web socket implementation for node, as this is a Browser-Feature
import "cross-fetch/polyfill"; to have a fetch functionality, which is used by ApolloClient
Now the app runs without errors, but I don't know why my subscription is not working. I have the same code on the frontend (a little bit different, as it is React) and it works well there.
This is my subscription logic:
const observable = client.subscribe({
query: gql`
subscription {
payments() {
amount
id
time
userId
}
}
`,
});
observable.subscribe((data: any) => {
console.log("data", data);
});
I am expecting that my console.log is logging some data, when the database changes - but it doesn't and my Hasura Dashboard lists no new connection.
How can I establish a websocket connection to my database?
I discovered, that the WebSocket uri on a Node.js client has to start with http/https and not ws/wws.
With this code it works now:
const wsLink = new WebSocketLink({
uri: "https://my.hasura.app/v1/graphql", // instead of "ws://my.hasura.app/v1/graphql"
options: {
reconnect: true,
},
webSocketImpl: ws,
});
I'm building a web app using Angular 7 for the frontend and NodeJS, MongoDB and ExpressJS for the backend. The application runs as intended as long as I interact with the application to navigate, but when I manually type the path, it seems like it doesn't hit the component for the route. It works all good locally, but not on Heroku.
Here's is my app-routing.module.ts:
const routes: Routes = [{
path: "login",
component : LoginComponent
},
{
path: "registration",
component : SignupComponent
},
{
path: "votes",
component : VotesComponent
},
{
path: "votes/:website",
component : VoteComponent
},
{
path: "**",
redirectTo: "votes"
}
];
#NgModule({
imports: [CommonModule, RouterModule.forRoot(routes, { enableTracing: false})],
exports: [RouterModule]
})
export class AppRoutingModule { }
All my routes in Express is prefixed with /api/, such as:
app.get('/api/votes', (req, res, next) => {
const limit = parseInt(req.query.limit)
db.getVotes(limit).then((votes) => {
res.json(votes);
});
});
I check the jwt like this:
app.use(
checkJwt({ secret: process.env.JWT_SECRET }).unless({ path: [
{ url: "/api/authenticate", methods: ['POST'] },
{ url: "/api/votes", methods: ["GET"]}]})
);
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
res.status(401).send({ error: 'Not authorized.' });
};
});
My call to votes is performed like this:
private baseUrl = environment.apiUrl;
this.http.get<any[]>(`${this.baseUrl}/votes`, this.httpOptions))).subscribe(data => {
this.data = data;
});
The baseUrl is defined as follows in the evironment.prod.ts:
export const environment = {
production: true,
apiUrl: '/api'
};
Here comes the problem. If I go to my deployment on Heroku and access https://myurl.com I'm redirected to https://myurl.com/votes and I can see the requested url is https://myurl.com/api/votes, so thats all fine and I get my angular application with all the data. But if I manually type in https://myurl.com/votes I get JSON back in the browser with error "Not authorized", and I can see that the requested URL is https://myurl.com/votes, instead of https://myurl.com/api/votes. Any ideas whats causing this? It seems like Angular is not routing it properly. When it's typed manually it doesn't hit any of my components.
For non-match route path, the router configuration should include below one.
{
path: "",
component: "404NotFoundComponent"
}