Skip to content

Functional Programming in TypeScript using the fp-ts library: Pipe and Flow Operator

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Functional programming (FP) is an approach to software development that uses pure functions to help developers in trying to create maintainable and easy-to-test software. FP is notable for its ability to efficiently parallelize pure functions. This approach should lead to code that is easier to reason about and test, and can often result in more concise and maintainable code.

What you will find in this post

Reading about “pure functions” or “parallelizing” things could be scary. Everything seems very “mathematical” and “theoretical”, but don’t be afraid: the goal of this post is to try to explain in a really practical way (theory and math will pop out only when really necessary) how to bring these core concepts and approach of FP in your TS project using Giulio Canti’s library fp-ts.

Pure and Impure functions

Pure functions always produce the same output for the same input, regardless of the context in which it is called.

Calling this type of function also has no side effects, meaning it does not modify any external state or interact with the outside world in any way. Here is an example of such pure function:

function doubleNumbers(numbers: number[]): number[] {
	return numbers.map((n) => n * 2);
}

This function takes an array of numbers, doubles each number in the array, and returns a new array with the doubled numbers. Here are some characteristics of this function that make it pure:

  1. No side effects: This function doesn't modify any external state outside of its own scope. It only operates on the input array, and returns a new array with the transformed values.

  2. Deterministic: Given the same input, this function will always return the same output. There are no random or unpredictable behaviors.

  3. No dependencies: This function doesn't rely on any external state or dependencies. It only depends on the input that's passed to it.

Because of these characteristics, this function is considered pure. It's predictable, testable, and doesn't introduce any unexpected behavior.

A side effect occurs in a program when you insert external code into your function. This prevents the function from “purely” performing its task.

An impure function in TypeScript is a function that, when called with the same arguments, may produce different results each time it is called. This is because an impure function can depend on mutable state or external factors, such as the current time, user input, or global variables, which can change between calls.

Here's an example of an impure function in TypeScript:

let counter = 0;

function incrementCounter(value: number): number {
	counter += value;
	return counter;		
}

In this example, the incrementCounter function modifies the global counter variable every time it is called. Since the function's return value depends on the current value of counter, calling the function with the same argument multiple times may produce different results.

For example, if we call the function like this:

incrementCounter(1); // returns 1

incrementCounter(1); // returns 2

incrementCounter(1); // returns 3  

The function's return value changes each time we call it because the value of counter is being modified by each call. This makes incrementCounter an impure function.

Note that impure functions can make it harder to reason about code since their behavior can be unpredictable. In general, it's best to minimize the use of impure functions and rely on pure functions, which have no side effects and always produce the same output for a given input.

Actions, Calculations, and Data - Thinking like a functional programmer

Let’s try to focus one moment on side effects.

The definition says FP avoids side effects, but side effects are the very reason we run our software. The definition seems to imply that we must try to completely avoid them when in reality, we use side effects when we have to. Functional programmers know side effects are necessary yet problematic, so they have a lot of tools for working with them.

A functional programmer first tries to classify the code into 3 general categories:

  • Actions

  • Calculations

  • Data

Actions depends on when they are called or how many times they are called.

Calculations and Data instead doesn’t depend on when or how many times they are called, the difference is only that calculations can be executed, data cannot; you don’t really know what calculation will do until you run it.

Functional programmers prefer data to calculations and calculations to actions and they try to build code by applying and composing as many “calculations” as possible in order to work with "solid" data (but also trying to “control” actions too).

Fp-Ts

There are many famous functional programming languages such as Haskell, Scala, Elm, Erlang, Elixir etc.

Fp-Ts provides developers with popular patterns and reliable abstractions from typed functional languages in TypeScript.

In this post, we will focus on the first two building blocks of fp-ts: Pipe and Flow operators.

Pipe Operator

The pipe operator is a built-in function in fp-ts that allows you to compose functions from left to right. It takes a value as its first argument and any number of functions as subsequent arguments (that accept only one argument of the same type of the returning type of the preceding function). The output of each function becomes the input of the next function in the pipeline, and the final result is returned.

import { pipe } from 'fp-ts/function';

const result = pipe(
	1,
	increment,
	double,
	decrement
);

function increment(n: number): number {
	return n + 1;
}  

function double(n: number): number {
	return n * 2;
}

function decrement(n: number): number {
	return n - 1;
}

In this example, we start with the number. Then increment it, double it, and finally decrement it. The pipe operator allows us to chain these functions together in a readable and concise way.

Why use the pipe operator?

The pipe operator is useful for a few reasons:

  1. It makes code more readable. By chaining functions together with the pipe operator, it is easy to see the flow of data through the functions.

  2. It makes code more concise. Without the pipe operator, we would have to nest function calls, which can be hard to read and understand.

  3. It makes code more modular. By breaking down a problem into smaller, composable functions, we can write more reusable code.

Flow Operator

We could say that the pipe operator is our basic brick to build our ecosystem, flow is really similar but it allows you to define a sequence of functions separately and then apply them to a value later.

The flow operator is also a built-in function in fp-ts, it takes any number of functions as arguments and returns a new function that applies each function in order.

import { flow } from 'fp-ts/function';

const addOne = (x: number) => x + 1;

const double = (x: number) => x * 2;

const addOneAndDouble = flow(addOne, double);

console.log(addOneAndDouble(2)); // Output: 6

In the example above, we create two simple functions addOne and double that takes a number and return a number. We then use the flow function from fp-ts to compose the two functions. The flow function takes any number of functions as arguments and returns a new function that applies each function in sequence from left to right.

By using the Flow operator, we don't need to explicitly define the type of the addOne and double functions' parameters. TypeScript can infer their types based on how they are used in the function composition. This makes the code shorter and more readable while still ensuring type safety.

Pros of using the Flow operator in fp-ts:

  1. Type inference: The Flow operator allows TypeScript to infer the types of function parameters, making the code shorter and more readable while still ensuring type safety.

  2. Composability: The Flow function provided by fp-ts makes it easy to compose multiple functions into a single function.

Cons of using the Flow operator in fp-ts:

  1. Learning curve: If you are new to functional programming, the Flow operator can be challenging to understand and use correctly.

  2. Debugging: Since the Flow operator relies on type inference, it can be challenging to debug issues related to type mismatches or incorrect function composition.

Differences between pipe and flow

The key difference between pipe and flow is in how they handle type inference. Pipe is better at inferring types, as it can use the output type of one function as the input type of the next function in the pipeline. Flow, on the other hand, requires explicit type annotations to be added to the composed functions.

When to use Pipe or Flow

Both pipe and flow are useful tools for composing functions and creating pipelines in a functional and declarative way. However, there are some situations where one might be more appropriate than the other.

Pipe is a good choice when you need to perform a linear sequence of transformations on a value in left-to-right order.

Flow is a good choice when you need to create a reusable function that encapsulates a series of transformations.

Conclusion

Fp-ts is a powerful library that provides many tools and abstractions for functional programming in TypeScript. The pipe and flow operators are just two of the many features that it offers. In this post we started to explore the building blocks of this library, learning their powers and their main differences. If you're interested in learning more about functional programming, explore some of these other concepts and check out the fp-ts documentation for more information, or wait for the next blog post here on This Dot Blog!

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

How to configure and optimize a new Serverless Framework project with TypeScript cover image

How to configure and optimize a new Serverless Framework project with TypeScript

How to configure and optimize a new Serverless Framework project with TypeScript If you’re trying to ship some serverless functions to the cloud quickly, Serverless Framework is a great way to deploy to AWS Lambda quickly. It allows you to deploy APIs, schedule tasks, build workflows, and process cloud events through a code configuration. Serverless Framework supports all the same language runtimes as Lambda, so you can use JavaScript, Ruby, Python, PHP, PowerShell, C#, and Go. When writing JavaScript though, TypeScript has emerged as a popular choice as it is a superset of the language and provides static typing, which many developers have found invaluable. In this post, we will set up a new Serverless Framework project that uses TypeScript and some of the optimizations I recommend for collaborating with teams. These will be my preferences, but I’ll mention other great alternatives that exist as well. Starting a New Project The first thing we’ll want to ensure is that we have the serverless framework installed locally so we can utilize its commands. You can install this using npm: ` From here, we can initialize the project our project by running the serverless command to run the CLI. You should see a prompt like the following: We’ll just use the Node.js - Starter for this demo since we’ll be doing a lot of our customization, but you should check out the other options. At this point, give your project a name. If you use the Serverless Dashboard, select the org you want to use or skip it. For this, we’ll skip this step as we won’t be using the dashboard. We’ll also skip deployment, but you can run this step using your default AWS profile. You’ll now have an initialized project with 4 files: .gitignore index.js - this holds our handler that is configured in the configuration README.md - this has some useful framework commands that you may find useful serverless.yml - this is the main configuration file for defining your serverless infrastructure We’ll cover these in more depth in a minute, but this setup lacks many things. First, I can’t write TypeScript files. I also don’t have a way to run and test things locally. Let’s solve these. Enabling TypeScript and Local Dev Serverless Framework’s most significant feature is its rich plugin library. There are 2 packages I install on any project I’m working on: - serverless-offline - serverless-esbuild Serverless Offline emulates AWS features so you can test your API functions locally. There aren’t any alternatives to this for Serverless Framework, and it doesn’t handle everything AWS can do. For instance, authorizer functions don’t work locally, so offline development may not be on the table for you if this is a must-have feature. There are some other limitations, and I’d consult their issues and READMEs for a thorough understanding, but I’ve found this to be excellent for 99% of projects I’ve done with the framework. Serverless Esbuild allows you to use TypeScript and gives you extremely fast bundler and minifier capabilities. There are a few alternatives for TypeScript, but I don’t like them for a few reasons. First is Serverless Bundle, which will give you a fully configured webpack-based project with other features like linters, loaders, and other features pre-configured. I’ve had to escape their default settings on several occasions and found the plugin not to be as flexible as I wanted. If you need that advanced configuration but want to stay on webpack, Serverless Webpack allows you to take all of what Bundle does and extend it with your customizations. If I’m getting to this level, though, I just want a zero-configuration option which esbuild can be, so I opt for it instead. Its performance is also incredible when it comes to bundle times. If you want just TypeScript, though, many people use serverless-plugin-typescript, but it doesn’t support all TypeScript features out of the box and can be hard to configure. To configure my preferred setup, do the following: 1. Install the plugins by setting up your package.json. I’m using yarn but you can use your preferred package manager. ` Note: I’m also installing serverless here, so I have a local copy that can be used in package.json scripts. I strongly recommend doing this. 2. In our serverless.yml, let’s install the plugins and configure them. When done, our configuration should look like this: ` 3. Now, in our newly created package.json, let’s add a script to run the local dev server. Our package.json should now look like this: ` We can now run yarn dev in our project root and get a running server 🎉 But our handler is still in JavaScript. We’ll want to pull in some types and set our tsconfig.json to fix this. For the types, I use aws-lambda and @types/serverless, which can be installed as dev dependencies. For reference, my tsconfig.json looks like this: ` And I’ve updated our index.js to index.ts and updated it to read as follows: ` With this, we now have TypeScript running and can do local development. Our function doesn’t expose an HTTP route making it harder to test so let’s expose it quickly with some configuration: ` So now we can point our browser to http://localhost:3000/healthcheck and see an output showing our endpoint working! Making the Configuration DX Better ion DX Better Many developers don’t love YAML because of its strict whitespace rules. Serverless Framework supports JavaScript or JSON configurations out of the box, but we want to know if our configuration is valid as we’re writing it. Luckily, we can now use TypeScript to generate a type-safe configuration file! We’ll need to add 2 more packages to make this work to our dev dependencies: ` Now, we can change our serverless.yml to serverless.ts and rewrite it with type safety! ` Note that we can’t use the export keyword for our configuration and need to use module.exports instead. Further Optimization There are a few more settings I like to enable in serverless projects that I want to share with you. AWS Profile In the provider section of our configuration, we can set a profile field. This will relate to the AWS configured profile we want to use for this project. I recommend this path if you’re working on multiple projects to avoid deploying to the wrong AWS target. You can run aws configure –profile to set this up. The profile name specified should match what you put in your Serverless Configuration. Individual Packaging Cold starts#:~:text=Cold%20start%20in%20computing%20refers,cache%20or%20starting%20up%20subsystems.) are a big problem in serverless computing. We need to optimize to make them as small as possible and one of the best ways is to make our lambda functions as small as possible. By default, serverless framework bundles all functions configured into a single executable that gets uploaded to lambda, but you can actually change that setting by specifying you want each function bundled individually. This is a top-level setting in your serverless configuration and will help reduce your function bundle sizes leading to better performance on cold starts. ` Memory & Timeout Lambda charges you based on an intersection of memory usage and function runtime. They have some limits to what you can set these values to but out of the box, the values are 128MB of memory and 3s for timeout. Depending on what you’re doing, you’ll want to adjust these settings. API Gateway has a timeout window of 30s so you won’t be able to exceed that timeout window for HTTP events, but other Lambdas have a 15-minute timeout window on AWS. For memory, you’ll be able to go all the way to 10GB for your functions as needed. I tend to default to 512MB of memory and a 10s timeout window but make sure you base your values on real-world runtime values from your monitoring. Monitoring Speaking of monitoring, by default, your logs will go to Cloudwatch but AWS X-Ray is off by default. You can enable this using the tracing configuration and setting the services you want to trace to true for quick debugging. You can see all these settings in my serverless configuration in the code published to our demo repo: https://github.com/thisdot/blog-demos/tree/main/serverless-setup-demo Other Notes Two other important serverless features I want to share aren’t as common in small apps, but if you’re trying to build larger applications, this is important. First is the useDotenv feature which I talk more about in this blog post. Many people still use the serverless-dotenv-plugin which is no longer needed for newer projects with basic needs. The second is if you’re using multiple cloud services. By default, your lambdas may not have permission to access the other resources it needs. You can read more about this in the official serverless documentation about IAM permissions. Conclusion If you’re interested in a new serverless project, these settings should help you get up and running quickly to make your project a great developer experience for you and those working with you. If you want to avoid doing these steps yourself, check out our starter.dev serverless kit to help you get started....

Testing a Fastify app with the NodeJS test runner cover image

Testing a Fastify app with the NodeJS test runner

Introduction Node.js has shipped a built-in test runner for a couple of major versions. Since its release I haven’t heard much about it so I decided to try it out on a simple Fastify API server application that I was working on. It turns out, it’s pretty good! It’s also really nice to start testing a node application without dealing with the hassle of installing some additional dependencies and managing more configurations. Since it’s got my stamp of approval, why not write a post about it? In this post, we will hit the highlights of the testing API and write some basic but real-life tests for an API server. This server will be built with Fastify, a plugin-centric API framework. They have some good documentation on testing that should make this pretty easy. We’ll also add a SQL driver for the plugin we will test. Setup Let's set up our simple API server by creating a new project, adding our dependencies, and creating some files. Ensure you’re running node v20 or greater (Test runner is a stable API as of the 20 major releases) Overview * index.js - node entry that initializes our Fastify app and listens for incoming http requests on port 3001 * app.js - this file exports a function that creates and returns our Fastify application instance * sql-plugin.js - a Fastify plugin that sets up and connects to a SQL driver and makes it available on our app instance Application Code A simple first test For our first test we will just test our servers index route. If you recall from the app.js code above, our index route returns a 501 response for “not implemented”. In this test, we're using the createApp function to create a new instance of our Fastify app, and then using the inject method from the Fastify API to make a request to the / route. We import our test utilities directly from the node. Notice we can pass async functions to our test to use async/await. Node’s assert API has been around for a long time, this is what we are using to make our test assertions. To run this test, we can use the following command: By default the Node.js test runner uses the TAP reporter. You can configure it using other reporters or even create your own custom reporters for it to use. Testing our SQL plugin Next, let's take a look at how to test our Fastify Postgres plugin. This one is a bit more involved and gives us an opportunity to use more of the test runner features. In this example, we are using a feature called Subtests. This simply means when nested tests inside of a top-level test. In our top-level test call, we get a test parameter t that we call methods on in our nested test structure. In this example, we use t.beforeEach to create a new Fastify app instance for each test, and call the test method to register our nested tests. Along with beforeEach the other methods you might expect are also available: afterEach, before, after. Since we don’t want to connect to our Postgres database in our tests, we are using the available Mocking API to mock out the client. This was the API that I was most excited to see included in the Node Test Runner. After the basics, you almost always need to mock some functions, methods, or libraries in your tests. After trying this feature, it works easily and as expected, I was confident that I could get pretty far testing with the new Node.js core API’s. Since my plugin only uses the end method of the Postgres driver, it’s the only method I provide a mock function for. Our second test confirms that it gets called when our Fastify server is shutting down. Additional features A lot of other features that are common in other popular testing frameworks are also available. Test styles and methods Along with our basic test based tests we used for our Fastify plugins - test also includes skip, todo, and only methods. They are for what you would expect based on the names, skipping or only running certain tests, and work-in-progress tests. If you prefer, you also have the option of using the describe → it test syntax. They both come with the same methods as test and I think it really comes down to a matter of personal preference. Test coverage This might be the deal breaker for some since this feature is still experimental. As popular as test coverage reporting is, I expect this API to be finalized and become stable in an upcoming version. Since this isn’t something that’s being shipped for the end user though, I say go for it. What’s the worst that could happen really? Other CLI flags —watch - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--watch —test-name-pattern - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--test-name-pattern TypeScript support You can use a loader like you would for a regular node application to execute TypeScript files. Some popular examples are tsx and ts-node. In practice, I found that this currently doesn’t work well since the test runner only looks for JS file types. After digging in I found that they added support to locate your test files via a glob string but it won’t be available until the next major version release. Conclusion The built-in test runner is a lot more comprehensive than I expected it to be. I was able to easily write some real-world tests for my application. If you don’t mind some of the features like coverage reporting being experimental, you can get pretty far without installing any additional dependencies. The biggest deal breaker on many projects at this point, in my opinion, is the lack of straightforward TypeScript support. This is the test command that I ended up with in my application: I’ll be honest, I stole this from a GitHub issue thread and I don’t know exactly how it works (but it does). If TypeScript is a requirement, maybe stick with Jest or Vitest for now 🙂...

Next.js Route Groups cover image

Next.js Route Groups

Starting from Next.js 13.4, Vercel introduced the App Router with a whole set of new and exciting features. The way we organize the routing in our application has changed radically compared to previous versions of Next.js, as well as the definition and usage of Layouts for our pages. In this article, we will focus on what is called Route Groups, their use cases, and how they can help us in our developer experience. Basic introduction to the new App Router In version 13, Next.js introduced a new App Router built on React Server Components, which supports shared layouts, nested routing, loading states, error handling, and more. The App Router works in a new directory named app Creating a page.tsx file inside the app/test-page folder allows you to define what users are going to see when they navigate to /test-page. So folder’s names inside app directory define your app routes. You can also have nested routes like this: In this case, the URL of your page will be /test-page/nested-page By default, components inside app are React Server Components. This is a performance optimization and allows you to easily adopt them, and you can also use Client Components. Layouts In Next.js, a Layout file is a special component that is used to define the common structure and layout of multiple pages in your application. It acts as a wrapper around the content of each page, providing consistent styling, structure, and functionality. The purpose of a Layout file is to encapsulate shared elements such as headers, footers, navigation menus, sidebars, or any other components that should be present on multiple pages. By using a Layout file, you can avoid duplicating code across multiple pages and ensure a consistent user experience throughout your application. To create a Layout file in Next.js, you typically create a separate component file, such as Layout.tsx, and define the desired layout structure within it. This component can then be imported and used on individual pages where you want to apply the shared layout. By wrapping your page content with the Layout component, Next.js will render the shared layout around each page, providing a consistent look and feel. This approach simplifies the management of common elements and allows for easy updates or modifications to the layout across multiple pages. Here is an example of how to use a Layout file with the new App Router Route Groups In Next.js, the folders in your app directory usually correspond to URL paths. But if you mark a folder as a Route Group, it won't be included in the URL path of the route. This means you can organize your routes and project files into groups without changing the URL structure. Route groups are helpful for: 1. Organizing routes into groups based on site sections, intent, or teams. 2. Creating nested layouts within the same route segment level: - You can have multiple nested layouts in the same segment, even multiple root layouts. - You can add a layout to only a subset of routes within a common segment. To create a route group inside your app folder you just need to wrap the folder’s name in parenthesis: (folderName) Since route groups won’t change the URL structure your page.tsx content will be shown under the/inside-route-group path. Use cases Route groups are amazing when you want to create multiple layouts inside your page: Or if you want to specify a layout for a specific group of pages You need to be careful because all the examples above can lead you to some misunderstanding. *What is root layout? The top-most layout is called the Root Layout. This required layout is shared across all pages in an application.* As you can see, the route folder in the two examples above always has a well-defined root layout. This means that the specific layouts we have defined for the various groups will not replace the root layout, but will be added to it. However, Route Groups also allow us to redefine the root layout. Specifically, they allow us to define different root layouts for different segments of pages. All we have to do is remove the common Root Layout file, create some Route groups, and re-define the different Root layout files for every group in the route folder: In this way, we will have pages with different root layouts, and our paths will once again not be affected by the folder name used in parentheses. Conclusion In conclusion, Next.js route groups offer a powerful and flexible solution for organizing and managing routes in your Next.js applications. By grouping related routes together, you can improve code organization, enhance maintainability, and promote code reusability. Route groups allow for the use of shared layout components, and the customization of root layouts for different segments of pages. With Next.js route groups, you can streamline your development process, create a more intuitive routing structure, and ultimately deliver a better user experience....

The simplicity of deploying an MCP server on Vercel cover image

The simplicity of deploying an MCP server on Vercel

The current Model Context Protocol (MCP) spec is shifting developers toward lightweight, stateless servers that serve as tool providers for LLM agents. These MCP servers communicate over HTTP, with OAuth handled clientside. Vercel’s infrastructure makes it easy to iterate quickly and ship agentic AI tools without overhead. Example of Lightweight MCP Server Design At This Dot Labs, we built an MCP server that leverages the DocuSign Navigator API. The tools, like `get_agreements`, make a request to the DocuSign API to fetch data and then respond in an LLM-friendly way. ` Before the MCP can request anything, it needs to guide the client on how to kick off OAuth. This involves providing some MCP spec metadata API endpoints that include necessary information about where to obtain authorization tokens and what resources it can access. By understanding these details, the client can seamlessly initiate the OAuth process, ensuring secure and efficient data access. The Oauth flow begins when the user's LLM client makes a request without a valid auth token. In this case they’ll get a 401 response from our server with a WWW-Authenticate header, and then the client will leverage the metadata we exposed to discover the authorization server. Next, the OAuth flow kicks off directly with Docusign as directed by the metadata. Once the client has the token, it passes it in the Authorization header for tool requests to the API. ` This minimal set of API routes enables me to fetch Docusign Navigator data using natural language in my agent chat interface. Deployment Options I deployed this MCP server two different ways: as a Fastify backend and then by Vercel functions. Seeing how simple my Fastify MCP server was, and not really having a plan for deployment yet, I was eager to rewrite it for Vercel. The case for Vercel: * My own familiarity with Next.js API deployment * Fit for architecture * The extremely simple deployment process * Deploy previews (the eternal Vercel customer conversion feature, IMO) Previews of unfamiliar territory Did you know that the MCP spec doesn’t “just work” for use as ChatGPT tooling? Neither did I, and I had to experiment to prove out requirements that I was unfamiliar with. Part of moving fast for me was just deploying Vercel previews right out of the CLI so I could test my API as a Connector in ChatGPT. This was a great workflow for me, and invaluable for the team in code review. Stuff I’m Not Worried About Vercel’s mcp-handler package made setup effortless by abstracting away some of the complexity of implementing the MCP server. It gives you a drop-in way to define tools, setup https-streaming, and handle Oauth. By building on Vercel’s ecosystem, I can focus entirely on shipping my product without worrying about deployment, scaling, or server management. Everything just works. ` A Brief Case for MCP on Next.js Building an API without Next.js on Vercel is straightforward. Though, I’d be happy deploying this as a Next.js app, with the frontend features serving as the documentation, or the tools being a part of your website's agentic capabilities. Overall, this lowers the barrier to building any MCP you want for yourself, and I think that’s cool. Conclusion I'll avoid quoting Vercel documentation in this post. AI tooling is a critical component of this natural language UI, and we just want to ship. I declare Vercel is excellent for stateless MCP servers served over http....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co