July 18th, 2025

Just another network framework?

I've never been able to find a network framework that's simple to use for both small projects and large projects that grow quickly. Some like URLSession and Alamofire are low-level and always require setting up an architecture around them. Others like Moya have attempted more user-friendly approaches, but struggle to organize cleanly in larger projects.

With the arrival of macros in Swift, clever developers have tried protocol-based approaches. A vision that makes the most of macros, but too inflexible in my opinion.

How to get the best of both worlds? A framework that's both simple to get started with but flexible for the most important needs. It's for these reasons that I designed NetworkKit.

  • Simple, flexible, and modern syntax.
  • A distinction between request and client.
  • Middleware, interceptor, logger.
  • Completely customizable. Perfect for both small and large projects.

Let me show you the basics of NetworkKit.

@Get("/users/:id/books")
struct GetBooks {
    @Path
    var id: String
 
    @Query
    let limit: Int
}
 
let request = GetBooks(id: "1234", limit: 10)
let client = Client("https://api.com")
 
let response: Response<[Book]> = try await client.perform(request)
let books = response.data

A declarative approach

NetworkKit uses a declarative approach for requests. It's intuitive, readable, and has a what you see is what you get feel. Using macros allowed me to achieve this kind of result:

@Get("/users/:id/books")
struct GetUserBooks {
    @Path
    var id: String
 
    @Query("s")
    let search: String
}

This syntax allows you to quickly build simple requests while maintaining good readability on more complex requests.

@Post("/users/:id/books")
struct PostUserBooks {
    @Path
    var id: String
 
    @Body
    struct Body {
        let name: String
        let author: Author
    }
}

An example of a POST request with a body.

Separating client and request

It's very common to work with multiple development environments (dev, staging, prod, etc.). Your server changes, but your requests stay the same. That's why NetworkKit separates the client from requests. It's thus possible to send the same request to your dev and prod servers, without having to declare the same thing twice.

@Get("/users/:id/books")
struct GetBooks {
    @Path
    let id: String
}
 
let request = GetBooks(id: "1234")
 
let dev = Client("https//localhost:3000")
let prod = Client("https://api.com")
 
let devResponse: Response<[Book]> = try await dev.perform(request)
let prodResponse: Response<[Book]> = try await prod.perform(request)

With this method, nothing is declared twice.

Moreover, on large projects, you can easily end up with hundreds of requests. When projects grow, organizing things by file in folders is very effective. With this approach, you can use Xcode's quick search to quickly access your requests.

Can it be adopted?

With the previous version of NetworkKit, I was hoping for some feedback. But I didn't get any. And that's completely normal. It's clear that for a developer to choose your framework over another more popular one, it had better be really solid. Over time, I was able to take a step back on how I was using it. Here are the points I worked on:

  • Improve the declarative syntax. I hope the simple but complex aspect will have its effect.
  • A more complete lifecycle for requests with middlewares and interceptors.
  • Strengthen the existing: make what already existed even simpler and more permissive.
  • Much better documentation.

I've been using NetworkKit since its beginnings and the new version for some time in my personal projects as well as with my clients. I'm convinced that this approach is reliable and it has proven capable of scaling on large projects.

If you're curious and want to test NetworkKit, you can find all installation instructions on the Github project. Don't hesitate to give me feedback!

looping arrowfollow me here!