Localized routes in koa - node.js

I'm developing a site with multiple languages. Some routes will therefore also have to be localized and I'm not sure how to do this properly.
I'm using #koa/router for routing.
For this example it's only English and Swedish but the site will handle more languages.
I can setup routes to match words in different languages like
router.get('/(create-account|skapa-konto)/', (ctx, next) => {
ctx.body = translate('signup_welcome');
await next();
});
But, I want the English site to only respond to '/sign-up' and send 404 for '/skapa-konto' (and vice versa).
In the real world the route would point to some controller function. So if I set up individual routes for each language I would have to change all localized routes manually should the controller function change in the future. That's something I would like to avoid ;)
Any suggestions?

I ended up solving this by extending the Router like this:
const LocalizedRouter = class extends Router {
/**
* Set up route mapping
* #param {object} options
*/
constructor(options) {
if (!Array.isArray(options.languages)) {
throw new TypeError('Languages must be of type Array');
}
super(options);
this.languages = options.languages;
}
/**
* Router function for GET method
* #param {string | Object<string, string>} RouteCollection
*/
get(routes, func) {
if (typeof(routes) === 'string') {
super.get(routes, func);
return;
}
if (typeof(routes) === 'object') {
for(const key in routes) {
if(!this.languages.includes(key)) {
continue;
}
if(typeof(func) !== 'function') {
throw new TypeError('Middleware must be a function');
}
const checkLanguageAndMount = async (ctx, next) => {
if(ctx.state.lang !== key) {
return next();
}
return func(ctx, next);
};
super.get(routes[key], checkLanguageAndMount);
}
return;
}
throw new TypeError('"Routes" must be a string or an object');
}
};
I can then set up my routes like this:
const myRouter = new LocalizedRouter({
languages: ['en', 'sv']
});
myRouter.get({
'en': '/create-account',
'sv': '/skapa-konto'
}, (ctx, next) => {
ctx.body = translate('signup_welcome');
await next();
};
This can probably be cleaned up but it does solve what I wanted to do.
EDIT: Fixed bug that caused 404 if two languages had identical paths

This problem interested me so I created a small github repo with some code. I'll try to explain here:
I created an array with some options:
const localeConfig = [
{
locale: "en",
routes: [
{
path: "/sign-up",
controllers: [enController],
method: "GET",
},
],
prefix: false,
},
{
locale: "se",
routes: [
{
path: "/skapa-konto",
controllers: [seController],
method: "GET",
},
],
prefix: false,
},
];
I then pass this object to a setupRoutes function that basically iterates the array, generating all the routes according to those options.
const setupRoutes = (localeConfig) => {
// Have some check to prevent duplicate routes
localeConfig.forEach((opt) => {
// Adding prefix according to option
const localePrefix = opt.prefix ? `/${opt.locale}` : "";
opt.routes.forEach((route) => {
const path = `${localePrefix}${route.path}`;
router[route.method.toLowerCase()].apply(router, [
path,
...route.controllers,
]);
});
});
};
So, for instance, if you were to change any of the controllers in either language you would only need to update the specific locale object.route.controllers. I imagine you could even have each different locale in a different file to have some modularity.
The github repo is here and I would really like to have you contribute to it if you have any idea on how to improve this.
Cheers!

Related

Im trying to implement authentication in next.js using next-auth and next.js middleware, but im getting an error when using it in every route

Im trying to implement auth logic using this example but I am trying to implement it in every route like so
export const config = {
matcher: ['/:path*'],
};
but my browser breaks because too many requests, but when I change the matcher array to specific routes, it works, like this:
matcher: ['/teacher','/student'],
with the matcher like this, auth works for the pages (student,teacher) but not other pages.
middleware.ts:
import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from 'next-auth/middleware';
const PUBLIC_FILE = /\.(.*)$/;
export default withAuth(
async function middleware(req: NextRequest) {
if (
req.nextUrl.pathname.startsWith('/_next') ||
req.nextUrl.pathname.includes('/api/') ||
PUBLIC_FILE.test(req.nextUrl.pathname)
) {
return;
}
if (req.nextUrl.locale === 'default') {
const locale = req.cookies.get('NEXT_LOCALE') || 'en';
return NextResponse.rewrite(
new URL(
`/${locale}${req.nextUrl.pathname}${req.nextUrl.search}`,
req.url,
),
);
}
},
{
callbacks: {
authorized({ req, token }) {
return !!token;
},
},
},
);
export const config = {
matcher: ['/:path*', '/teacher'],
};

ExpressJs: Route with selected parameters only

Is this possible to add the condition directly in the route itself?
Something like this...
...
Router.get('/:status(active|inactive)', index);
...
I know that it can be handled by middleware or in the index method via conditions. However, If this is possible then this way can save a lot of efforts.
Currently I am writing this way:
routes/category.js
Router.get('/:status?', index);
Category/Controller.js
...
const index = async (req, res) => {
try {
const pageParams = paginationParams(req.query.page, configs.perPage);
const conditions = (typeof req.params.status === 'undefined') ? {} : (
['active', 'inactive'].includes(req.params.status) ? req.params.status : {}
);
const count = await Model.count(conditions);
const items = await Model.find(conditions, {
__v: false,
}, {
sort: {
status: -1,
title: 1,
}
})
.skip(pageParams.serialNumber)
.limit(configs.perPage);
...
} catch (error) {
res.status(500).send();
}
};
...
Actually, Router.get('/:status(active|inactive)', index); should work as expected by restricting the possible values for the status parameter to either active or inactive.
You should then be able to retrieve the status parameter as usual in your function with:
const { status } = req.params;
If you don't specify the status parameter as active or inactive on your request URL, the page will 404.
For more, you can read this interesting article on the topic.

DELETE method requires query parameter instead of path parameter in next.js

I am creating a CRUD in API,
but the delete does not seems to work properly.
I get a response from this
http://localhost:3000/api/admin/categories?id=1
and not from this
http://localhost:3000/api/admin/categories/1
this is the code in next.js:
export default async (req, res) => {
const {
query: { id },
method,
} = req;
switch (method) {
case "DELETE":
return res.status(200).json({
success: true,
id: id,
});
}
in React:
axios.delete(`http://localhost:3000/api/admin/categories/`, {id: 1})
The same situation is also with "PUT" method
Folder Directory:
api
|
---admin
--------categories
--------index.js
If you want to create API Routes such as DELETE, PUT ( they require /id as path parameter), you should create a separate file in that folder.
Like this:
api
|
---admin
--------categories
--------index.js
--------[id].js
And in the [id].js file:
export default function handler(req, res) {
const { id } = req.query;
if (req.method === "DELETE") {
res.end(`Category: ${id}`);
}
}

How to get domain in middleware, then pass it to components/store before axios calls [Nuxt]

I want to access the domain name before any axios calls are made. Because the axios calls need the domain URL to make them.
So, I want to get the domain first, then pass it to the store/components so they can load properly.
My understanding is that the domain is held in the req object of the context which is passed to the midddleware.
How would I get it and then pass it to the store and the components?
You can do it like this. Use nuxtServerInit. This function is called on the serverside, and only once if you reload the page. For this you need to go to your store and add this:
//store
actions: {
nuxtServerInit(store, context){
store.commit("setUrl", context.req.originalUrl);
}
}
Well thats all. People also like to destructure the arguments:
actions: {
nuxtServerInit({ commit},{req}){
commit("setUrl", req.originalUrl);
}
}
I am not sure if its originalUrl or just url... or maybe something different.
To expand on lfaruki's answer, you can do it that way. Here is my solution:
store/index.js
export const state = () => ({
domain: '',
});
export const mutations = {
setDomain(state, domain) {
state.domain = domain;
},
};
export const actions = {
nuxtServerInit(store, context) {
store.commit('setDomain', context.req.headers.host);
},
};
export const getters = {
domain: (state) => state.domain,
};
middleware/domain.js
export default function ({ route, store, redirect }) {
console.log('hey look at that', store.getters['domain']);
}
nuxt.config.js
export default {
...
router: {
middleware: 'domain'
},
}

hapijs - serve static files from directory handler with variable path name

I'm trying to do something like this in my routes file using hapijs + inert plugin
{
method: 'GET',
path: '/l/{path*}',
handler: {
directory: {
path: (req) => {
const defaultDir = '/public';
return getHostInfo(req.headers.host).then((d) => {
if (!d || !d.directory) return defaultDir;
return d.directory;
}).catch((e) => {
return defaultDir;
});
}
}
}
},
path parameter expects a string, array or function which returns a string or array...in my case, my function returns a promise...so it doesn't work.
I tried adding hapi-as-promised package which modifies reply function to support then method but did't work.
Basically I want to serve static assets from one directory or another depending on the host header value.
Well, the only thing i have in my mind is this hack. First, you have to make an extension point to make your async stuff:
server.ext({
type: `onPreHandler`,
method: (request, reply) => {
const defaultDir = '/public';
return getHostInfo(request.headers.host).then((d) => {
if (!d || !d.directory) {
request.app.dirPath = defaultDir;
return;
}
request.app.dirPath = d.directory;
}).catch((e) => {
request.app.dirPath = defaultDir;
}).then(() => {
return reply.continue();
});
}
});
And then the route:
{
method: `get`,
path: `/l/{path*}`,
handler: {
directory: {
path: (request) => {
return request.app.dirPath;
}
}
}
}
I don't know how right is this way, but i tested and it worked. So i hope this helps. And i gotta notice, using node to server files on production isn't a common way by some reason.

Resources