Skip to main content

· 4 min read
Raul Mihaila

Vercel is a platform to host frontend applications and static sites but you can also host an express app using serverless functions. I've been using it for quite some time (mainly for frontend stuff) but it can also deploy your Expressjs backend! We will see how to create a Bridge app and deploy it to Vercel.

About Bridge.

Bridge is a Typescript Nodejs framework that focuses on type-safety and developer experience. It aims to provide the best developer experience by simplifying the process of developing and integrating APIs.

This is done by heavily using the Typescript inference between elements from the backend such as data validation, routes and middlewares.

Prerequisites

Having Nodejs and Vercel CLI installed on your machine.

1. Create your Bridge app

The easiest way it to use the create-bridge-app npm package.

You can do that by using:

bash
npx create-bridge-app@latest
bash
npx create-bridge-app@latest

Then, enter your project name when prompted and select the "minimal-express" template. Once the app is created, navigate to the project directory and install the dependencies with the following command:

cd project-name && npm i
cd project-name && npm i

Step 2: Prepare your app for deployment

By default, a Bridge app is not compatible with Vercel's serverless functions. To make it work, you need to make some changes to the app.

  • Create a vercel.json file
  • Edit the index.ts file
  • Edit the tsconfig.json file
  • Edit the package.json file

vercel.json

Create a "vercel.json" file at the root of your project with the following content:

json
{
"rewrites": [
{
"source": "/api/:path+",
"destination": "/api"
}
],
"redirects": [
{
"source": "/",
"destination": "/api"
}
]
}
json
{
"rewrites": [
{
"source": "/api/:path+",
"destination": "/api"
}
],
"redirects": [
{
"source": "/",
"destination": "/api"
}
]
}

This file configures Vercel to redirect all requests to the "/api" route.

index.ts

To make it work, we'll also need to update our index.ts file and initBridge on the /api route. We additionnaly add a new route GET "/api/hello".

ts
import { initBridge, handler, method } from 'bridge';
import express from 'express';
const app = express();
app.get('/api', (req, res) => res.send(`Welcome on Bridge API`));
const heyHandler = handler({
resolve: () => {
return 'Hey you!';
},
});
const routes = {
hey: method({ GET: heyHandler }),
};
app.use('/api', initBridge({ routes }).expressMiddleware());
app.listen(3000, () => {
console.log(`Listening on port 3000`);
});
module.exports = app;
ts
import { initBridge, handler, method } from 'bridge';
import express from 'express';
const app = express();
app.get('/api', (req, res) => res.send(`Welcome on Bridge API`));
const heyHandler = handler({
resolve: () => {
return 'Hey you!';
},
});
const routes = {
hey: method({ GET: heyHandler }),
};
app.use('/api', initBridge({ routes }).expressMiddleware());
app.listen(3000, () => {
console.log(`Listening on port 3000`);
});
module.exports = app;

Don't forget to add the module.exports = app at the end of your file.

package.json

Update your "package.json" file with the following.

json
{
"name": "bridge-vercel",
"version": "1.0.0",
"dependencies": {
"bridge": "^2.0.45",
"express": "^4.18.2",
"ts-node": "^10.9.1"
},
"scripts": {
"deploy": "tsc && vercel",
"deploy-prod": "tsc && vercel --prod",
"dev": "ts-node index.ts"
},
"devDependencies": {
"@types/express": "^4.17.15",
"@types/node": "^18.11.16",
"typescript": "^4.9.4"
}
}
json
{
"name": "bridge-vercel",
"version": "1.0.0",
"dependencies": {
"bridge": "^2.0.45",
"express": "^4.18.2",
"ts-node": "^10.9.1"
},
"scripts": {
"deploy": "tsc && vercel",
"deploy-prod": "tsc && vercel --prod",
"dev": "ts-node index.ts"
},
"devDependencies": {
"@types/express": "^4.17.15",
"@types/node": "^18.11.16",
"typescript": "^4.9.4"
}
}
  • npm run dev will run our development server on port 8080.
  • npm run vercel builds and deploys our app to vercel in preview
  • npm run deploy-prod build and deploys our app to vercel in production

tsconfig.json

Finally, update your "tsconfig.json" file with the following content:

json
{
"compilerOptions": {
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "commonjs" /* Specify what module code is generated. */,
"outDir": "api" /* Specify an output folder for all emitted files. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
json
{
"compilerOptions": {
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "commonjs" /* Specify what module code is generated. */,
"outDir": "api" /* Specify an output folder for all emitted files. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

Now everything should be ready for the deployment.

npm run deploy
npm run deploy

Now you should have a fully working Bridge app deployed to Vercel. With Bridge, you can create complex APIs with a very good developer experience while using TypeScript and its type system to its fullest extent.

To read more about Bridge, check out the documentation.

· 3 min read
Raul Mihaila

We often are asked how Bridge is different from trpc. They can be splitted into 2 differents branches: differences at client level and server level.

The way you would consume the client

Bridge Studio (the tool used to build the SDK of a Bridge api) is a more traditional REST codegen app. It's way of functioning is closer to something like Graphql codegen or OpenAPI Generator.

tRPC on the other hand lets you see real-time updated of your servers's routes and types. It has no build or compile steps, meaning no code generation, runtime bloat or build step. It makes it a great choice when building app as a full stack developer as everything is close.

This difference leads to other significant variances:

  • tRPC couples your server & website/app more tightly: you're less likely to have missmatched versions between your server/client
  • tRPC requires the backend and frontend code on the same machine (thus making it great for monorepos!)
  • because it's less coupled than tRPC, Bridge lets you build any public-faced API (meant to be used by other external developers). It can be published as an NPM package.
  • Bridge can build SDK in more languages. It's great for apps that have different (or multiple) languages between frontend and backend.

The way you would write your server

We can highlight some other differences in the synthax used to write your sever code.

Bridge relies more on objects while tRPC is more functional (it uses function chaining). OOP can be used with Bridge. Here is the same request with Bridge (a handler) and tRPC (a procedure).

Bridge:

ts
import { z } from "zod"
import { handler } from 'bridge';
const helloHandler = handler({
body: z.object({ name: z.string() }),
resolve: ({ body }) => `Hello ${body.name}`
})
ts
import { z } from "zod"
import { handler } from 'bridge';
const helloHandler = handler({
body: z.object({ name: z.string() }),
resolve: ({ body }) => `Hello ${body.name}`
})

Trpc:

ts
import { initTRPC, router } from '@trpc/server';
import { z } from "zod";
const appRouter = router({
greet: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => ({ greeting: `hello, ${input}!` })),
});
ts
import { initTRPC, router } from '@trpc/server';
import { z } from "zod";
const appRouter = router({
greet: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => ({ greeting: `hello, ${input}!` })),
});

Moreover, Bridge can be used with classes (for those who like object-oriented programmation) and its capabilities (like inheritance or polymorphisms).

ts
class Session {
login: handler({
// ...
})
}
// the user class now inherits the "login" handler from the session
class User extends Session {
getUser = handler({
//...
})
}
const routes = {
// ...
user: new User(),
// ...
}
ts
class Session {
login: handler({
// ...
})
}
// the user class now inherits the "login" handler from the session
class User extends Session {
getUser = handler({
//...
})
}
const routes = {
// ...
user: new User(),
// ...
}

· 3 min read
Raul Mihaila

Migrating a project from one framework to another can be a challenging task, but it can also bring numerous benefits such as improved performance, increased functionality, and better security. Whether you're facing outdated dependencies, changing business requirements, or simply seeking to explore new technologies, there comes a time when every project must undergo a transition.

But such thing is not always possible and sometimes the cost and effort required may not make it feasible. It is essential to weigh the benefits against the costs and determine whether migrating to a new framework is the right decision for your project.

Why would you transition to Bridge?

Bridge is a TypeScript framework that leverages the same mechanisms as Express.js, but with an added layer of structure for improved development speed and efficiency. Its main objective is to provide type-safety across all components of your API, including middlewares, data validation, and endpoint handlers.

Additionally, Bridge can significantly reduce the boilerplate code required for a TypeScript Node.js application, making the development process faster and more streamlined. Whether you're looking to improve your backend or frontend development, Bridge provides a powerful solution that leverages the benefits of TypeScript to deliver a more efficient and scalable application.

Mixing Bridge and expressjs

The good news is that while Bridge and Expressjs are different, they all share the same fundamental mechanisms and can co-exist inside the same project.

This means that you can continue to use your existing project while gradually incorporating new routes built with Bridge. The full migration can eventually be achieved over time.

This compatibility is achieved by integrating Bridge as an Express.js middleware, making the transition process seamless and straightforward. With this approach, you can take advantage of the benefits of Bridge while maintaining the stability and functionality of your existing project.

ts
import express from 'express';
import { handler, initBridge } from 'bridge';
const app = express();
// ...
// your existing express stuff
// ...
// Bridge
const routes = { hello: handler({ resolve: () => 'Hello' }) };
app.use('/bridge', initBridge({ routes }).expressMiddleware());
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});
ts
import express from 'express';
import { handler, initBridge } from 'bridge';
const app = express();
// ...
// your existing express stuff
// ...
// Bridge
const routes = { hello: handler({ resolve: () => 'Hello' }) };
app.use('/bridge', initBridge({ routes }).expressMiddleware());
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});

In conclusion, migrating a project to a new framework can be a challenging but rewarding experience that brings numerous benefits. Whether you're looking to improve performance, increase functionality, or explore new technologies, the process of migrating to Bridge can deliver the results you need. With its compatibility with Express.js and its ability to provide type-safety across all components of your API, Bridge offers a powerful solution for modern development.