Reescrevendo a StarWars API em Deno
Tags: deno, typescript, rest, svelte
Published: 2020-06-23 11:23:28

_Créditos da capa: [Dimitrij Agal](https://masbagal.com/)_

## 👉 TL;DR;

Reescrevi a API do https://swapi.dev utilizando Deno e Svelte. Disponível para testes através do https://swapi-deno.azurewebsites.net/ e do [DockerHub](https://hub.docker.com/r/rodolphoalves/swapi-deno?utm_source=docker4win_2.3.1.0&utm_medium=repo_open&utm_campaign=referral)

O código da API + Frontend está disponível no [GitHub](https://github.com/rodolphocastro/deno-swapi).

{% github rodolphocastro/deno-swapi no-readme %}

## 💭 Inspiração

Quem nunca se inspirou em um projeto existente para auxiliar no aprendizado de uma nova linguagem de programação ou Tecnologia?

Eu sou culpado de fazer isso. Direto. Chega a dar vergonha de volta e meia olhar meu GitHub e ver tantos projetos que começo e acabo pausando só _pra dar uma olhadinha_ em alguma outra tecnologia que me chamou a atenção 😅

O projeto da vez, _inspirado pelas primeiras versões "production ready" do [Deno](https://deno.land/)_, foi a reescrita da [Star Wars API](https://swapi.dev). Faz tempos que utilizo a **Swapi** para testar Apps (Web e Mobile) que precisam fazer chamadas a APIs REST e sempre me "frustou" um pouco ela não ter sido atualizada com os dados da trilogia mais recente!

> Nota: Não que a versão v0.2.0 deste projeto esteja com os dados recentes! 😂

## 🦕 Deno

Para aqueles que não estão acompanhando o mundo "Node":

> Deno é um runtime simples, moderno e seguro para a execução de Type/Javascript, utilizando a engine V8 e desenvolvido através do RUST
_(Fonte: https://deno.land/, traduzido pelo autor)_

Em suma a ideia do Deno é pegar todo o aprendizado da comunidade com o NodeJS manter o que é bom e refinar o que "precisa" ser refinado.

Em minha opinião algumas grandes vantagens do Deno são:

1. Suporte nativo a **Typescript**
2. Versionamento integrado ao Git (nada de `packages.json` ou o inferno do `node_modules`)
3. Possui um conjunto "nativo" de bibliotecas suportadas `std` (o que me lembra bastante o .NET)

Caso esteja lendo no futuro: Lembre-se que este post foi escrito com base na versão v1.1.1 do Deno! Então algumas coisas ainda eram novas!

### Escolhendo nossas dependências

Antes de começar a bater código comecei olhando as bibliotecas que já existiam no ecossistema Deno para fazer duas tarefas primordias de uma API REST:

+ Servir o conteúdo através dos endpoints
+ Armazenar, de alguma maneira, o conteúdo.

Começando por servir o conteúdo vi que o Deno possui vários "sabores" de frameworks para APIs REST, alguns são:

1. [Oak](https://deno.land/x/oak)
2. [Drash](https://deno.land/x/drash)
3. [Dactyl](https://deno.land/x/dactyl) [_obs: Baseado no Oak_]

No momento em que comecei o projeto o que me pareceu mais tentador foi o **Oak**, especialmente por ser diferente do padrão *dotNet Core* ao qual estou acostumado 😅.

Em seguida precisava de uma maneira de armazenar o conteúdo de maneira prática. Já existem **várias** bibliotecas para conectar com bancos SQL e NoSQL, porém para manter o menor _footprint_ possível para a API pensei em embarcar os dados junto à API. 

Olhei nas bibliotecas `std` e econtrei o que precisava para lidar com arquivos: a biblioteca **fs**.

### Sugestões para o ambiente de desenvolvimento

Antes de passarmos ao código, algumas sugestões:

Para desenvolver a API e o Portal utilizei o [Visual Studio Code](https://code.visualstudio.com/) com as extensions:

+ [Deno](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno)
+ [Svelte](https://marketplace.visualstudio.com/items?itemName=JamesBirtles.svelte-vscode)
+ [Svelte Intellisense](https://marketplace.visualstudio.com/items?itemName=ardenivanov.svelte-intellisense)

### Lendo os dados de arquivos json

Comecei modelando os dados disponíveis na [API Original](https://swapi.dev) e os transcrevendo para seis interfaces diferentes:

1. `Film`
2. `Person`
3. `Planet`
4. `Specie`
5. `Starship`
6. `Vehicle`

Utilizando o [Insomnia](https://insomnia.rest/) acessei os endpoints da API Original e extrai os dados disponíveis publicamente, higienizando alguns dados (como os "ids") e removendo o envelope.

Desta maneira construi alguns arquivos `.json` no seguinte formato, para armazenar os dados junto à API:

```json
{
  "data": [
    { ... }
  ]
}
```

Com este formato estabelecido abstrai criei um arquivo para cada um dos `models` e escrevi algumas interfaces e classes para consolidar a lógica de carregar os dados a partir de arquivos e disponibilizados em um `Array` de seu devido tipo `T`:

```typescript
// As of v0.55.0 this module requires the --unstable flag to be used
import * as fs from "https://deno.land/std@v0.55.0/fs/mod.ts";

/**
 * Describes the expected structure for a .json storage.
 */
interface JsonStorable<T> {
  data?: T[];
}

/**
 * Loads data and deserializes data from a json file.
 * @param dataDir directory containing the file
 * @param filename filename containing the data, serialized
 */
async function loadDataFromFiles<T>(
  dataDir: string,
  filename: string,
): Promise<T[]> {
  await fs.ensureDir(dataDir);
  const result = await fs.readJson(dataDir + "/" + filename) as JsonStorable<T>;
  return result.data ?? [];
}
```

Para permitir, eventualmente, a abstração para outra fonte de dados também criei uma estrutura (baseada bem vagamente no Vuex) para armazenar estes dados na aplicação, como um singleton:

```typescript
export interface IState<T> {
  list(): T[];
}

export class ModelState<T> implements IState<T> {
  constructor(
    private readonly values: T[] = [],
  ) {}

  list(): T[] {
    return this.values;
  }
}
```

Finalmente, para casar tudo, criei algums `factory methods` para gerar meus `States` com base nos `.json`:

```typescript
/**
 * Creates and seeds a ModelState for Films.
 * @param dataDir directory holding the json file, defaults to ./data
 * @param filmFile json file containing films, defaults to films.json
 */
export async function createFilmStateAsync(
  dataDir: string = "./data",
  filmFile: string = "films.json",
): Promise<IState<Film>> {
  const films = await loadDataFromFiles<Film>(dataDir, filmFile);
  return new ModelState<Film>(films);
}

/**
 * Creates and seeds a ModelState for Species.
 * @param dataDir directory holding the json file, defaults to ./data
 * @param speciesFile json file containing species, defautls to species.json
 */
export async function createSpecieStateAsync(
  dataDir: string = "./data",
  speciesFile: string = "species.json",
): Promise<IState<Specie>> {
  const species = await loadDataFromFiles<Specie>(dataDir, speciesFile);
  return new ModelState<Specie>(species);
}

/**
 * Creates and seeds a ModelState for Vehicles.
 * @param dataDir directory holding the json file, defaults to ./data
 * @param vehiclesFile json file containing species, defautls to vehicles.json
 */
export async function createVehicleStateAsync(
  dataDir: string = "./data",
  vehiclesFile: string = "vehicles.json",
): Promise<IState<Vehicle>> {
  const vehicles = await loadDataFromFiles<Vehicle>(dataDir, vehiclesFile);
  return new ModelState<Vehicle>(vehicles);
}

/**
 * Creates and seeds a ModelState for Starships.
 * @param dataDir directory holding the json file, defaults to ./data
 * @param starshipsFile json file containing starships, defaults to startships.json
 */
export async function createStarshipStateAsync(
  dataDir: string = "./data",
  starshipsFile: string = "starships.json",
): Promise<IState<Starship>> {
  const starships = await loadDataFromFiles<Starship>(dataDir, starshipsFile);
  return new ModelState<Starship>(starships);
}

/**
 * Creates and seeds a ModelState for Planets.
 * @param dataDir directory holding the json file, defaults to ./data
 * @param planetsFile json file containing planets, defaults to planets.json
 */
export async function createPlanetsStateAsync(
  dataDir: string = "./data",
  planetsFile: string = "planets.json",
): Promise<IState<Planet>> {
  const planets = await loadDataFromFiles<Planet>(dataDir, planetsFile);
  return new ModelState<Planet>(planets);
}

/**
 * Creates and Seeds a ModelState for People.
 * @param dataDir directory holding the json file, defaults to ./data
 * @param peopleFile json file containing people, defaults to people.json
 */
export async function createPeopleStateAsync(
  dataDir: string = "./data",
  peopleFile: string = "people.json",
): Promise<IState<Person>> {
  const people = await loadDataFromFiles<Person>(dataDir, peopleFile);
  return new ModelState<Person>(people);
}
```

Com tudo isso pronto, vamos aos endpoints!

### Servindo os dados através de endpoints com o Oak

A premissa do **Oak** é que temos uma `Application` e podemos adicionar diversos `Middlewares` a ela. Desta maneira a própria camada do Oak irá gerar, para nós, as chamadas necessárias para fazer o `Http-Server` do `Deno` trabalhar conforme esperamos!

Para os fins da API vamos utilizar o `RouterMiddleware`. Este middleware nos permite especificar `functions` para lidar com os *verbos http* em *rotas específicas*.

Por exemplo, para os endpoints de **listar** e **obter um específico** a implementação com o Oak fica assim:

```typescript
// Criando a Application do Oak
const app = new Application();

// Assuma que o filmsState já está criado
const filmsRouter = new Router({ prefix: "/api/films" });
filmsRouter
  // Indicando que este router deverá escutar na rota GET /
  .get("/", ({ response }) => {
    response.body = filmsState.list();
    response.status = Status.OK;
  })
  // E na rota GET /:id
  .get("/:id", ({ response, params }) => {
    const { id } = params;
    const result = filmsState.list().filter((f) =>
      f.url === parseInt(id as string)
    )[0];
    if (result) {
      response.status = Status.OK;
      response.body = result;
      return;
    }

    response.status = Status.NotFound;
  });

// Adicionando nossos middlewares à Aplicação
app.use(
  ...[
    filmsRouter.routes()
  ]
);

// e, finalmente, rodando nossa aplicação
await app.listen({ port: 8000 });
```

O resto dos endpoints seguem todos o mesmo padrão, salvo o endpoint que apresenta os arquivos do nosso frontend!

## 🤖 Svelte

Após terminar os 6 endpoints originais comecei a pensar como seria interessante ter um SwaggerUI ou alguma interface documentando a API. Porém, *no momento*, **não existem bibliotecas para gerar a OpenApi** spec a partir dos endpoints implementados.

> Ou seja, não temos algo como o [Swagger Plugin do NestJS](https://docs.nestjs.com/recipes/swagger) ou o [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) do .NET Core.

Então, para aproveitar o embalo, decidi sair da zona de conforto de Vue + TS e dar a cara a tapa para testar outro framework Javascript que muitas pessoas estão adotando: *Svelte*

A ideia do *Svelte* é similar à do React e do Vue: **Um único arquivo (chamado de component) agrega a lógica, o estilo visual e a estrutura para exibição.**. Com a mesma "pegada" do Vue o Svelte é **reativo por padrão**. (Com algumas diferenças!)

Vindo do Vue as principais diferenças que senti ao usar o Svelte foram:

1. Menos "verboso" para declarar componentes
2. Não depender de um CLI para um kickstart da configuração
3. Utilização de 'blocos' para condicionais e loops, ao invés de atributos no HTML

No geral eu gostei, bastante, da breve experiência com o Svelte e pretendo utiliza-lo mais no futuro. Admito que fiquei tentado a ir atrás de como utilizar Typescript junto ao Svelte mas como a ideia era implementar logo, deixei de lado!

### Components do Svelte

A sintaxe de components do Svelte é a seguinte:

```svelte
<script>
// Código javascript
</script>

<style>
# CSS do component
</style>

<!-- Corpo HTML -->
```

O principal component do Frontend é o "Browse" genérico. A ideia aqui é que como a estrutura é sempre a mesma (afinal, não estou fazendo um Portal, apenas exibindo possíveis retornos!) um único component, configurável, dá conta de exibir o que é necessário!

Os elementos que notei que repetiam entre cada exibição de dados eram: O endpoint em si, o nome do endpoint, um emoji para exibir no `heading` e quais propriedades do elemento deviam ser exibidas na lista interna.

O component resultante dessa parametrização é:

```svelte
<script>
  export let endpointName = "Generic Endpoint";
  export let endpointEmoji = "❓";
  export let displayProperties = ["url", "name"];
  export let endpoint;

  const endpointPromise = fetchData();

  let showJson = false;
  let showList = false;

  async function fetchData() {
    const response = await fetch(endpoint);
    return response.json();
  }

  function toggleList() {
    showList = !showList;
  }

  function toggleJson() {
    showJson = !showJson;
  }
</script>

<section container>
  <h3 id="{endpointName}">{endpointEmoji} {endpointName}</h3>
  <p>
    The {endpointName} endpoint is served on the route
    <code>{endpoint}</code>
  </p>
  {#await endpointPromise}
    <p>Please wait, loading data...</p>
  {:then dataResult}
    <p>
      There are {dataResult.length} objects of type {endpointName} on the API
    </p>
    <hr />
    <button on:click={toggleJson}>
      {showJson ? 'Hide Json' : 'Show Json'}
    </button>
    <button on:click={toggleList}>
      {showList ? 'Hide list' : 'Show list'}
    </button>
    {#if showJson}
      <h4>JSON</h4>
      <p>A {endpointName}'s JSON looks like this:</p>
      <pre>
        <code>{JSON.stringify(dataResult[0], null, '\t')}</code>
      </pre>
    {/if}
    {#if showList}
      <h4>Result from the API</h4>
      <ul>
        {#each dataResult as data}
          <li>{data[displayProperties[0]]}: {data[displayProperties[1]]}</li>
        {/each}
      </ul>
    {/if}
  {:catch _}
    <p>
      Ops, something went wrong while fetching data! Please refresh the page
    </p>
  {/await}
</section>

```

A cara final do portal ficou assim:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/c0p23kgln98766xx03va.png)

### Servindo nossa SPA através do Oak

A última alteração necessária foi adicionar um novo `Middleware` ao Oak, apontando ao servidor que os arquivos do subdiretório `./portal/public` deviam ser publicados na rota `/` do servidor!

O código resultante é:

```typescript
// Criando o middleware
const servePortal: Middleware = async ctx => {
  await send(ctx, ctx.request.url.pathname, {
    root: Deno.cwd()+'/portal/public',
    index: 'index.html'
  })
};

// Indicando que ele deve ser utilizando, junto aos outros
app.use(
  ...[
    filmsRouter.routes(),
    speciesRouter.routes(),
    vehiclesRouter.routes(),
    starshipRouter.routes(),
    planetsRouter.routes(),
    peopleRouter.routes(),
  ],
  servePortal
);
```

## ❗ Considerações Finais

O projeto está longe de finalizado, ainda tenho alguns itens do *Roadmap* a ser sanados (como habilitar CORS, atualizar os dados e melhorar a tipagem!) porém estou satisfeito com o resultado atual!

Meu foco no futuro próximo será criar uma imagem `Docker` da aplicação e hospeda-la em algum lugar, espero que de graça 😅

Quem quiser ver o **código** completo, o *roadmap* e o *histórico de alterações* pode encontrar tudo isso no repositório [GitHub](https://github.com/rodolphocastro/deno-swapi).

Obrigado por lerem este post e até a próxima!