DEV Community

Kamil Afsar
Kamil Afsar

Posted on

Introducing Samen: end-to-end typesafe APIs for TypeScript devs (no GraphQL needed)

When GraphQL was introduced by Facebook we were excited. Facebook produces lots of high quality projects. It had to be interesting and useful, right? Well, while I think GraphQL certainly has its uses, I'm convinced 99% of us don't really need it.

GraphQL shines when it comes to generating clients for multiple languages. Or when you have a non JS/TS backend. Also for public APIs like GitHub's, Spotify's or Shopify's, it's really cool to have a tool to easily generate a client for it with a familiar interface.

But when you're a fullstack TypeScript developer, like us, and you need to create both the server and the client, GraphQL is just like a funnel sitting in between them. It adds a lot of unnecessary bloat, complexity and confusion.

We dreamed up an RPC style API framework and decided to build it.

Introducing Samen

Let me show you real quick how it works in 3 steps.

If you prefer video over text: scroll to the bottom of the article.

Step 1: Expose your backend functions

The only thing you need is a file called samen.ts in the root of your backend project. In this file you export the functions you need to call from your frontend(s). You organise your functions in what we call "services", by using Samen's createService API. Like so:

import { createService } from "@samen/server"

async function sayHello(name: string): Promise<string> {
  return `Hello, ${name}`
}

async function sayGoodbye(name: string): Promise<string> {
  return `Goodbye, ${name}`
}

export const greetingService = createService({
  sayHello,
  sayGoodbye,
})
Enter fullscreen mode Exit fullscreen mode

Step 2: Run samen

Samen comes with a long running process to watch, compile and generate code. When you run npx samen in your project directory it will generate a file called samen.generated.ts in your frontend. This file contains an SDK for your Samen backend.

Step 3: Call your backend functions

You can now import SamenClient from the samen.generated file, which you can use to call the functions on your backend. The only thing you need to do is pass Samen a fetch library of your choice (which Samen uses for the actual HTTP calls).

import fetch from "isomorphic-unfetch"

import { SamenClient } from "../samen.generated"

// provide Samen your favourite fetch library
const samen = new SamenClient(fetch)

const helloMessage = await 
  samen.greetingService.sayHello(
    "Steve Jobs",
  )

console.log(helloMessage) // prints "Hello, Steve Jobs"

const goodbyeMessage = await 
  samen.greetingService.sayGoodbye(
    "Steve Wozniak",
  )

console.log(goodbyeMessage)  // prints "Goodbye, Steve Wozniak"

Enter fullscreen mode Exit fullscreen mode

And that's really all there is. As you can see it's almost like you're calling a function on your frontend. That's the magic of Samen.

Sharing models between server and client

The best part is that Samen will automatically analyse your exposed backend functions and put all the models you need in your SDK. Now you can just use anything you want. Well, almost anything. Samen supports interfaces, enums and type aliases. It does not support classes.

Here's how it would work:

import { createService } from "@samen/server"

interface Message {
  text: string
}

async function sayHello(name: string): Promise<Message> {
  return { text: `Hello, ${name}` }
}

async function sayGoodbye(name: string): Promise<Message> {
    return { text: `Goodbye, ${name}` }
}

export const greetingService = createService({
  sayHello,
  sayGoodbye,
})
Enter fullscreen mode Exit fullscreen mode

Now, you can import the Message model on your frontend. Like this:

import fetch from "isomorphic-unfetch"

import { SamenClient, Message } from "../samen.generated"

// provide Samen your favourite fetch lib
const samen = new SamenClient(fetch)

const helloMessage: Message = await 
  samen.greetingService.sayHello(
    "Steve Jobs",
  )

console.log(helloMessage) // prints { text: "Hello, Steve Jobs" }

const goodbyeMessage: Message = await 
  samen.greetingService.sayGoodbye(
    "Steve Wozniak",
  )

console.log(goodbyeMessage) // prints { text: "Goodbye, Steve Wozniak" }
Enter fullscreen mode Exit fullscreen mode

TypeScript is much more expressive than GraphQL. Because there's no intermediate language involved you can just use all features you want in your models that TypeScript has to offer: generics, conditional types, indexed types, etc. You are 100% in control of your data models.

But there's one more feature I want to show you.

Error handling

An important but overlooked part of APIs is error handling. As you know handling errors really sucks with GraphQL backends (and it's not a lot better in regular HTTP/REST). We think Samen handles this so much better.

Let's say we want to our sayHello function to throw an error when the parameterised name is too short. Like this:

class NameTooShortError extends Error {
}

async function sayHello(name: string): Promise<Message> {
  if (name.length < 3) {
    throw new NameTooShortError()
  }
  return {
    text: `Hello, ${name}`,
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling errors on your client looks like this:

import { SamenClient, HelloMessage, NameTooShortError } from "../samen.generated"

// provide Samen your favourite fetch lib
const samen = new SamenClient(fetch)


try {
  const helloMessage: HelloMessage = await 
    samen.exampleService.sayHello(
      "", // oops!
    )
} catch (e) {
  if (e instanceof NameTooShortError) {
    alert("Name is too short!")
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see you now can import the Error class from your SDK and start handling it.

It gets even better. You can also put properties on your errors. Like this:

class NameTooShortError extends Error {
  constructor(public readonly minimumLength: number) {
    super()
  }
}
Enter fullscreen mode Exit fullscreen mode

Which comes in handy when you're handling your errors on the frontend:

} catch (e) {
  if (e instanceof NameTooShortError) {
    alert(`Name is too short, it should be at least ${e.minimumLength} characters!`)
  }
}
Enter fullscreen mode Exit fullscreen mode

Samen has a lot more to offer like middleware. But that's for another post.

Video

If you like to see Samen in action, here's a video where my friend Jasper tells about how Samen works:

We'd love to hear from you!

We are very excited to launch Samen. We think it's a fresh take on API development for TypeScript developers. We hope you give it a try and we hope to hear from you.

Top comments (0)