We recently published performance benchmarks showing how Encore.ts achieves 9x request throughput compared to Express.js, and 2x compared to Fastify.
But it seems that almost every new JavaScript framework these days claims that it's "lightning fast" or "lightweight", with "clean syntax" or a "minimalistic API". Sound familiar?
Understandably, the cynicism and scepticism is relatable, and I can't really blame you if you're thinking "do we really need yet another JavaScript backend framework?".
Still, I'd like to give it a go to convince you otherwise, by explaining how Encore.ts is different. Why it isn't just another backend framework, and why this time it actually might be worth paying a bit closer attention.
Encore.ts focuses heavily on providing an incredible developer experience, and takes this much further than just providing clean syntax or a tiny bundle size.
When you start your Encore.ts backend you'll be greeted by Encore's Local Development Dashboard.
This dashboard provides easy access to everything you need to rapidly develop and debug your application. Check it out:
Encore provides a live log of all requests, complete with a full trace of exactly what happened. This includes things like API calls, database queries, Pub/Sub messages, logging, and of course the complete request and response payloads.
The Local Development Dashboard provides automatic API documentation for all your API endpoints.
This is powered by Encore.ts's static analysis, which directly processes your TypeScript type definitions. As a result the API documentation is always up to date, and always accurate.
Never again worry about keeping the OpenAPI spec up-to-date.
Encore.ts also comes with an API Explorer that makes calling your API endpoints extremely simple. It's similar to tools like Postman, but much more powerful.
Because Encore.ts understands exactly what your API looks like it is pre-populated with all of your API endpoints and the exact fields they expect, including things like HTTP headers and query parameters.
And like Postman you can save frequently used requests, and easily share them with your team.
Encore.ts is designed from the ground up to support building complex, distributed systems. It supports building not just a single backend service, but an entire network of services that communicate with each other.
For more complex systems like this, the Local Development Dashboard provides a built-in architecture diagram that represents your entire system, and which services depend on each other.
It even includes infrastructure resources, like databases and Pub/Sub topics and subscriptions. More on that below.
Typical backend frameworks focus on making it easy to define API endpoints. But modern, cloud-native backends require so much more than that.
Modern backend applications make use of cloud infrastructure resources like databases, Pub/Sub topics, Cron Jobs, secrets, and so on.
Once you start breaking down your backend into multiple independent services, using Pub/Sub for event-driven communication is a must-have to ensure your system stays in sync and can scale horizontally.
It's high time for backend frameworks to step up their game and realize this is a core part of building a backend application. Encore.ts does just that, by providing built-in support for using such infrastructure resources directly in the framework.
For example, to define a Pub/Sub topic is just a few lines of code:
import { Topic } from "encore.dev/pubsub";
interface UserSignupEvent {
userID: string;
}
export const userSignups = new Topic<UserSignupEvent>(
"user-signup", {deliveryGuarantee: "at-least-once"});
Then just start publishing messages: await userSignups.publish({userID: "123"})
.
Defining a Pub/Sub subscription is just as easy:
import { Subscription } from "encore.dev/pubsub";
const _ = new Subscription(userSignups, "notify-crm-system", {
handler: async (event) => {
// Notify CRM system of new signup.
},
});
The best part of all of this is that Encore.ts's static analysis understands this, and automatically creates and configures the Pub/Sub topic when you run your application.
And when you deploy to the cloud you can easily wire up the topic to a real Pub/Sub topic in your cloud provider. Encore.ts comes with built-in integrations with AWS and Google Cloud. If you decide to use the Encore Platform it can even automatically provision the necessary infrastructure for you, directly into your own cloud account.
If you're familiar with the cloud you've probably heard of (or used) tools like Terraform, AWS CDK or Pulumi. They're tools that allow you to describe the infrastructure you need using code, and then automatically provision that infrastructure.
Compared to clicking around in the cloud provider console this offers several benefits, like reproducibility and version control. But it also has several drawbacks:
Local development: The infrastructure setup is entirely specific to the cloud provider, making anything but the simplest setup difficult to run locally.
Error prone: Differences between local development and production can cause bugs that are difficult to catch before they cause production issues.
Cloud Provider-specific: Since code is entirely specific to your cloud provider, you end up being quite locked-in to that provider.
Production-centric: Since you tailor the infrastructure setup heavily towards your production setup, it's very difficult to have a different setup for testing or staging environments. This causes those environments to be very expensive as they mirror production unnecessarily closely. And any divergence from that causes additional complexity, leading to bugs and incidents.
Encore.ts solves all these problems by defining the infrastructure resources directly in code, in a cloud provider-agnostic way. Encore.ts provides built-in adapters for major cloud providers, so you can easily switch between them while preserving the application semantics.
And last but not least, Encore automatically sets up all the necessary infrastructure locally, so you can easily develop and test your application locally without having to write a bunch of mocks or docker-compose manifests.
If you're familiar with TypeScript you might know that it needs to be transpiled into JavaScript to actually run it. This involves stripping out all the type information. As a result, all those nice type annotations you've added to the code have no effect at runtime.
This isn't normally a problem, except when it comes to user input. You might write a type-safe API endpoint definition that expects two fields to be set, and the code looks flawless. But at runtime there's nothing stopping a user from sending a request with only one of those fields set, or using the wrong field name, or sending the wrong type (a string instead of a number, for example).
This has led to a bunch of libraries that allow you to define the API schema in a way that can be used at runtime to validate the request. Perhaps the most popular of these is Zod, but there are many others. This is great, but the syntax is much more verbose and noisy compared to the clean, concise TypeScript type syntax.
Encore.ts improves on this by using the natural TypeScript types directly. When Encore.ts compiles your application it parses the TypeScript types and generates a JSON schema from them, which it then uses to automatically validate incoming requests. As a result, you get the best of both worlds: the clean, concise TypeScript syntax, and runtime schema validation.
Encore.ts also makes it easy to define API endpoints that combine data from different sources: some fields from the request body, others from the query parameters, and yet others from the HTTP headers. It looks like this:
interface Request {
// Maximum number of items to return. Parsed from the query parameters.
limit?: Query<number>;
// Custom header that must be set. Parsed from the HTTP headers.
myHeader: Header<"X-My-Header">;
// Regular field. Parsed from the request body.
type: "sprocket" | "widget";
}
export const myEndpoint = api<Request, Response>(
{ expose: true, method: "POST", path: "/my/endpoint" },
async (req) => {
// Implementation...
}
);
Just take a look at these two examples, comparing Zod to Encore.ts:
const headersSchema = z.object({
"x-foo": z.string(),
});
const queryStringSchema = z.object({
name: z.string().optional(),
excitement: z.number().optional(),
});
const bodySchema = z.object({
someKey: z.string().optional(),
someOtherKey: z.number().optional(),
requiredKey: z.array(z.number()),
nullableKey: z.number().nullable().optional(),
multipleTypesKey: z.union([z.boolean(), z.number()]).optional(),
enumKey: z.enum(["Alice", "Bob"]).optional(),
});
interface Schema {
foo: Header<"x-foo">;
name?: Query<string>;
excitement?: Query<number>;
someKey?: string;
someOtherKey?: number;
requiredKey: number[];
nullableKey?: number | null;
multipleTypesKey?: boolean | number;
enumKey?: "Alice" | "Bob";
}
I know which one I'd rather write. And read. And maintain.
Hopefully this gives you a good idea of what Encore.ts is all about, and that it's more than "just another backend framework". It's Open Source, so give it a try and let us know what you think. You can find us on Discord if you have any questions or feedback.